Files

2858 lines
103 KiB
JavaScript

import { processBlankSlateProfile, resolveBlankSlateLaunch } from "./blank-slate-profile.mjs";
import { execFileSync, spawn } from "node:child_process";
import { createServer } from "node:http";
import net from "node:net";
import { existsSync, readFileSync } from "node:fs";
import {
cp,
mkdir,
readFile,
readdir,
rename,
rm,
stat,
writeFile,
} from "node:fs/promises";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globalOpencodeConfigDir, workspaceOpencodeConfigCandidates } from "@openwork/paths";
import { configureFakeMediaForTests, installMediaPermissionHandlers } from "./media-permissions.mjs";
import { registerMigrationIpc } from "./migration.mjs";
import { createRuntimeManager, createSystemCaCertificateVerifyProc } from "./runtime.mjs";
import { registerUpdaterIpc } from "./updater.mjs";
import {
checkComputerUsePermissions,
getComputerUseMcpCommand,
listRunningApps,
openComputerUseSetupApp,
} from "./computer-use.mjs";
import { createUiControlServer } from "./ui-control-server.mjs";
import { createApplicationMenu } from "./app-menu.mjs";
import { applyBrandAppName } from "./brand-app-name.mjs";
import { createBrowserPanel } from "./browser-panel.mjs";
import { createWorkspaceStore } from "./workspace-store.mjs";
import {
buildNukeManifest,
executeNukeFreshStart,
runPendingNukeCleanup,
} from "./nuke.mjs";
import {
createConnectLinkReplayGuard,
extractConnectExchange,
resolveConnectExchangeUrl,
verifyConnectLinkUrl,
} from "./connect-link.mjs";
import {
applyDesktopBootstrapBrandIcon,
persistConnectLinkBranding,
} from "./connect-link-branding.mjs";
import { resolveConnectLinkPublicKeys } from "./connect-link-keys.mjs";
import { openExternalUrl } from "./open-external.mjs";
import { resolveAppIdentifier, resolveUserDataPath } from "./dev-profile.mjs";
import { fetchAgentContextDiagnosticsResponse } from "./agent-context-diagnostics-fetch.mjs";
import { downloadBinaryToPath, uploadMultipartFromBytes } from "./binary-transfer.mjs";
import {
createLinuxDesktopIntegration,
} from "./linux-desktop-integration.mjs";
import { createDesktopAutomationRunner, normalizeRunnerBaseUrl } from "./automation-runner.mjs";
import {
desktopActivationRequired,
enterprisePreactivationCommandAllowed,
resolveDesktopDistribution,
} from "./desktop-distribution.mjs";
import {
applyWindowsTaskbarIcon,
windowsBrandAppUserModelId,
windowsBrandShortcutDetails,
windowsBrandShortcutFileName,
windowsInstalledShortcutFileName,
windowsInstalledExecutablePath,
writeWindowsBrandShortcut,
windowsIconFromNativeImage,
} from "./brand-icon-windows.mjs";
import { resetMacDockIcon } from "./brand-icon-darwin.mjs";
import { createDesktopVaultKeyProvider } from "./secure-vault-key.mjs";
import {
clearOpenworkSentrySession,
initOpenworkSentry,
setOpenworkSentrySession,
} from "./sentry.mjs";
import { installStdioErrorHandlers } from "./stdio-errors.mjs";
import {
createRendererCrashRecovery,
installSocketTypeOfServiceGuard,
runDetachedTask,
} from "./process-resilience.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const APP_ROOT = path.resolve(__dirname, "../../..");
const require = createRequire(import.meta.url);
const desktopPackageMetadata = require("../package.json");
// Electron 35 eagerly resolves every export in a named ESM import, including
// safeStorage. Loading through CommonJS keeps safeStorage lazy so isolated demo
// profiles do not show macOS's native keychain dialog before our switches run.
const {
app,
BrowserWindow,
dialog,
ipcMain,
nativeImage,
nativeTheme,
net: electronNet,
powerMonitor,
Notification: ElectronNotification,
session,
shell,
systemPreferences,
} = require("electron");
const pty = require(["node", "pty"].join("-"));
const NATIVE_DEEP_LINK_EVENT = "openwork:deep-link-native";
const AUTOMATION_RUNNER_CREDENTIAL_REJECTED_EVENT = "openwork:automation-runner:credential-rejected";
const isDevMode = process.env.OPENWORK_DEV_MODE === "1";
const DESKTOP_DISTRIBUTION = resolveDesktopDistribution({
isPackaged: app.isPackaged,
packageFlavor: Reflect.get(desktopPackageMetadata, "openworkDistribution"),
environmentFlavor: process.env.OPENWORK_DESKTOP_DISTRIBUTION,
});
const TAURI_APP_IDENTIFIER = DESKTOP_DISTRIBUTION.appIdentifier;
const DEV_APP_IDENTIFIER = `${DESKTOP_DISTRIBUTION.appIdentifier}.dev`;
const DESKTOP_PROTOCOL_SCHEME = DESKTOP_DISTRIBUTION.protocolScheme;
const DEFAULT_APP_NAME =
(!app.isPackaged ? process.env.OPENWORK_ELECTRON_APP_NAME?.trim() : "") ||
(isDevMode ? `${DESKTOP_DISTRIBUTION.appName} - Dev` : DESKTOP_DISTRIBUTION.appName);
const BLANK_SLATE_LAUNCH = resolveBlankSlateLaunch({
appName: DEFAULT_APP_NAME,
profile: processBlankSlateProfile,
});
const APP_NAME = BLANK_SLATE_LAUNCH.appName;
let currentDisplayAppName = APP_NAME;
installStdioErrorHandlers();
installSocketTypeOfServiceGuard();
await initOpenworkSentry({
app,
distribution: DESKTOP_DISTRIBUTION,
packageMetadata: desktopPackageMetadata,
});
const BASE_APP_IDENTIFIER = isDevMode ? DEV_APP_IDENTIFIER : TAURI_APP_IDENTIFIER;
const APP_IDENTIFIER = resolveAppIdentifier({
appIdentifierOverride: process.env.OPENWORK_ELECTRON_APP_IDENTIFIER,
appRootPath: APP_ROOT,
baseAppIdentifier: BASE_APP_IDENTIFIER,
devAppIdentifier: DEV_APP_IDENTIFIER,
devProfile: process.env.OPENWORK_DEV_PROFILE,
isDevMode,
isPackaged: app.isPackaged,
});
if (BLANK_SLATE_LAUNCH.enabled || process.env.OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN === "1") {
// Fresh, isolated development profiles otherwise trigger macOS's native
// "Login" keychain prompt as soon as Chromium persists an authenticated
// cookie. That modal blocks the entire Electron main loop and makes the demo
// appear frozen. Production never sets this flag and continues to use the
// system keychain normally.
app.commandLine.appendSwitch("use-mock-keychain");
}
const RELEASE_DOWNLOAD_BASE_URL = "https://github.com/different-ai/openwork/releases/latest/download";
const RELEASE_PAGE_URL = "https://github.com/different-ai/openwork/releases/latest";
const DOCS_PAGE_URL = "https://openworklabs.com/docs";
const applicationMenu = createApplicationMenu({
appName: APP_NAME,
docsUrl: DOCS_PAGE_URL,
getWindow: () => createMainWindow(),
});
const uiControlServer = createUiControlServer({
app,
appName: APP_NAME,
appIdentifier: APP_IDENTIFIER,
getWindow: () => createMainWindow(),
});
const terminalProcesses = new Map();
let nextTerminalId = 1;
function defaultTerminalShell() {
if (process.platform === "win32") return process.env.COMSPEC || "powershell.exe";
return process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
}
async function resolveTerminalCwd(cwd) {
const fallback = os.homedir();
if (typeof cwd !== "string" || !cwd.trim()) return fallback;
const candidate = path.resolve(cwd);
const info = await stat(candidate).catch(() => null);
return info?.isDirectory() ? candidate : fallback;
}
function terminalForSender(event, terminalId) {
const terminal = terminalProcesses.get(String(terminalId ?? ""));
if (!terminal || terminal.webContentsId !== event.sender.id) return null;
return terminal;
}
function killTerminal(terminalId) {
const terminal = terminalProcesses.get(terminalId);
if (!terminal) return;
terminalProcesses.delete(terminalId);
try { terminal.process.kill(); } catch { /* already gone */ }
}
function killTerminalsForWebContents(webContentsId) {
for (const [terminalId, terminal] of terminalProcesses.entries()) {
if (terminal.webContentsId === webContentsId) killTerminal(terminalId);
}
}
// Production Electron shares the same on-disk state folder as the Tauri shell
// so in-place migration is a no-op for almost every file. Dev mode uses the
// separate dev identifier so it can run beside the production app.
//
// Dev profile precedence: OPENWORK_ELECTRON_USERDATA (explicit profile path)
// wins over everything; then OPENWORK_ELECTRON_APP_IDENTIFIER; then
// OPENWORK_DEV_PROFILE in unpackaged dev; then the legacy identifier default.
app.setName(APP_NAME);
app.setAppUserModelId(APP_IDENTIFIER);
if (BLANK_SLATE_LAUNCH.homePath) app.setPath("home", BLANK_SLATE_LAUNCH.homePath);
if (
app.isPackaged
&& !BLANK_SLATE_LAUNCH.enabled
&& process.env.OPENWORK_ELECTRON_DISABLE_PROTOCOL_REGISTRATION !== "1"
&& !(process.platform === "linux" && process.env.APPIMAGE)
) {
app.setAsDefaultProtocolClient(DESKTOP_PROTOCOL_SCHEME);
}
const userDataPath = BLANK_SLATE_LAUNCH.userDataPath ?? resolveUserDataPath({
appDataPath: app.getPath("appData"),
appIdentifier: APP_IDENTIFIER,
userDataOverride: process.env.OPENWORK_ELECTRON_USERDATA,
});
app.setPath("userData", userDataPath);
const linuxDesktopIntegration = createLinuxDesktopIntegration({
app,
dialog,
appName: APP_NAME,
distribution: DESKTOP_DISTRIBUTION.flavor,
});
// Resolve and cache the app icon (reused for BrowserWindow + mac dock).
// Packaged builds ship icons via electron-builder config, but for `dev:electron`
// the Electron default icon is shown without this.
function resolveAppIconPath() {
const candidates = [
// Dev: match Tauri's separate dev icon so the dev app is visibly distinct.
...(isDevMode
? [
path.resolve(__dirname, "../resources/icons/dev/icon.png"),
path.resolve(__dirname, "../resources/icons/dev/128x128@2x.png"),
path.resolve(__dirname, "../resources/icons/dev/icon-dev.icns"),
]
: []),
// Repo-relative path to the Electron resource icon set.
path.resolve(__dirname, "../resources/icons/icon.png"),
// Packaged Windows and Linux builds ship runtime icons via extraResources.
path.join(process.resourcesPath ?? "", "icons", "linux", "512x512.png"),
path.join(process.resourcesPath ?? "", "icons", "icon.png"),
];
for (const candidate of candidates) {
if (candidate && existsSync(candidate)) return candidate;
}
return null;
}
function normalizeRuntimeArch(value) {
const normalized = String(value || "").trim().toLowerCase();
if (["arm64", "aarch64", "arm64e"].includes(normalized)) return "arm64";
if (["x64", "x86_64", "amd64"].includes(normalized)) return "x64";
return normalized || "unknown";
}
function isMacRunningUnderRosetta() {
if (process.platform !== "darwin" || process.arch !== "x64") return false;
try {
return execFileSync("/usr/sbin/sysctl", ["-in", "sysctl.proc_translated"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim() === "1";
} catch {
return false;
}
}
function resolveSystemArch() {
if (process.platform === "darwin" && isMacRunningUnderRosetta()) return "arm64";
if (process.platform === "win32") {
return normalizeRuntimeArch(
process.env.PROCESSOR_ARCHITEW6432 || process.env.PROCESSOR_ARCHITECTURE || os.arch(),
);
}
if (typeof os.machine === "function") return normalizeRuntimeArch(os.machine());
return normalizeRuntimeArch(os.arch());
}
function platformDownloadSlug() {
if (process.platform === "darwin") return "mac";
if (process.platform === "win32") return "win";
return "linux";
}
function downloadAssetArch(arch) {
if (process.platform === "linux" && arch === "x64") return "x86_64";
return arch;
}
function downloadAssetExtension() {
if (process.platform === "darwin") return "dmg";
if (process.platform === "win32") return "exe";
return "AppImage";
}
function updaterManifestName(arch) {
if (process.platform === "darwin") return "latest-mac.yml";
if (process.platform === "win32") return "latest.yml";
return arch === "arm64" ? "latest-linux-arm64.yml" : "latest-linux.yml";
}
function archLabel(arch) {
if (arch === "arm64") return "ARM";
if (arch === "x64") return "Intel";
return arch;
}
function parseUpdaterManifestFiles(raw) {
const files = [];
let current = null;
for (const line of String(raw || "").split(/\r?\n/)) {
const start = line.match(/^\s*-\s+url:\s*(.+?)\s*$/);
if (start) {
current = { url: start[1].trim().replace(/^['"]|['"]$/g, "") };
files.push(current);
continue;
}
const prop = line.match(/^\s{4}([A-Za-z][A-Za-z0-9_-]*):\s*(.+?)\s*$/);
if (prop && current) {
current[prop[1]] = prop[2].trim().replace(/^['"]|['"]$/g, "");
}
}
return files.filter((file) => file.url);
}
function selectDownloadFile(files, arch) {
const assetArch = downloadAssetArch(arch);
const expected = `-${assetArch}-`;
const extension = downloadAssetExtension();
const matchingArch = files.filter((file) => file.url.includes(expected));
return (
matchingArch.find((file) => file.url.endsWith(`.${extension}`)) ||
matchingArch.find((file) => file.url.endsWith(".zip")) ||
matchingArch[0] ||
null
);
}
async function resolveCorrectArchitectureDownloadUrl(arch) {
const manifestUrl = `${RELEASE_DOWNLOAD_BASE_URL}/${updaterManifestName(arch)}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const response = await electronNet.fetch(manifestUrl, {
signal: controller.signal,
headers: { Accept: "text/yaml, text/plain, */*" },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const selected = selectDownloadFile(parseUpdaterManifestFiles(await response.text()), arch);
if (!selected?.url) return null;
return /^https?:\/\//i.test(selected.url)
? selected.url
: new URL(selected.url, `${RELEASE_DOWNLOAD_BASE_URL}/`).toString();
} catch (error) {
console.warn("[architecture] failed to resolve latest download URL", error);
return null;
} finally {
clearTimeout(timeout);
}
}
async function resolveArchitectureInfo() {
const appArch = normalizeRuntimeArch(process.arch);
const systemArch = resolveSystemArch();
const version = app.getVersion();
const targetArch = systemArch === "arm64" || systemArch === "x64" ? systemArch : appArch;
const assetName = `openwork-${platformDownloadSlug()}-${downloadAssetArch(targetArch)}-${version}.${downloadAssetExtension()}`;
const latestDownloadUrl = await resolveCorrectArchitectureDownloadUrl(targetArch);
const hasCorrectArchitectureDownload = Boolean(latestDownloadUrl);
return {
appArch,
appArchLabel: archLabel(appArch),
systemArch,
systemArchLabel: archLabel(systemArch),
mismatch: appArch !== systemArch && hasCorrectArchitectureDownload,
platform: process.platform === "win32" ? "windows" : process.platform,
version,
downloadUrl: latestDownloadUrl || `${RELEASE_DOWNLOAD_BASE_URL}/${assetName}`,
releaseUrl: RELEASE_PAGE_URL,
};
}
const APP_ICON_PATH = resolveAppIconPath();
const APP_ICON_IMAGE = APP_ICON_PATH ? nativeImage.createFromPath(APP_ICON_PATH) : null;
const BRAND_ICON_MAX_BYTES = 2 * 1024 * 1024;
const BRAND_ICON_FETCH_TIMEOUT_MS = 10_000;
// Keep in sync with ee/apps/den-api/src/brand-icon-validation.ts so logo CDNs
// that expect a browser request behave the same at save time and apply time.
const BRAND_ICON_FETCH_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
function scheduleBlankSlateProfileCleanup() {
if (!BLANK_SLATE_LAUNCH.rootPath) return;
try {
const child = spawn(process.execPath, [
path.join(__dirname, "blank-slate-cleanup.mjs"),
String(process.pid),
BLANK_SLATE_LAUNCH.rootPath,
], {
detached: true,
stdio: "ignore",
windowsHide: true,
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
});
child.once("error", (error) => {
console.warn("[blank-slate] failed to schedule temporary profile cleanup", error);
});
child.unref();
} catch (error) {
console.warn("[blank-slate] failed to schedule temporary profile cleanup", error);
}
}
let brandIconApplySequence = 0;
let brandIconRuntimeState = { applied: false, sourceUrl: null, reason: null };
function brandIconCachePath() {
return path.join(app.getPath("userData"), "brand-icon.png");
}
function brandIconSidecarPath() {
return path.join(app.getPath("userData"), "brand-icon.json");
}
function brandIconWindowsPath() {
return path.join(app.getPath("userData"), "brand-icon.ico");
}
function defaultAppWindowsIconPath() {
return path.join(app.getPath("userData"), "openwork-stock.ico");
}
let cachedWindowsProgramsPath = null;
function windowsProgramsPath() {
if (cachedWindowsProgramsPath) return cachedWindowsProgramsPath;
const userProfile = app.getPath("userData").split(/[\\/]AppData[\\/]/i)[0];
cachedWindowsProgramsPath = path.join(userProfile, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs");
return cachedWindowsProgramsPath;
}
function windowsBrandShortcutPath() {
return path.join(windowsProgramsPath(), windowsBrandShortcutFileName(currentDisplayAppName));
}
function windowsInstalledShortcutPath() {
return path.join(windowsProgramsPath(), windowsInstalledShortcutFileName(APP_NAME));
}
function windowsBrandShortcutMarkerPath() {
return path.join(app.getPath("userData"), "windows-brand-shortcut.txt");
}
function windowsExecutablePath() {
return windowsInstalledExecutablePath({
packaged: app.isPackaged,
execPath: app.getPath("exe"),
resourcesPath: process.resourcesPath,
shortcutPath: windowsBrandShortcutPath(),
});
}
async function readWindowsBrandShortcutMarker() {
return (await readFile(windowsBrandShortcutMarkerPath(), "utf8").catch(() => "")).trim();
}
function repairWindowsShortcutTarget(shortcutPath, details) {
const payload = Buffer.from(JSON.stringify({ shortcutPath, ...details }), "utf8").toString("base64");
const script = [
`$value = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json`,
"$shell = New-Object -ComObject WScript.Shell",
"$link = $shell.CreateShortcut($value.shortcutPath)",
"$link.TargetPath = $value.target",
"$link.WorkingDirectory = $value.cwd",
"$link.Description = $value.description",
"$link.IconLocation = \"$($value.icon),$($value.iconIndex)\"",
"$link.Save()",
].join("\n");
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
windowsHide: true,
});
}
async function registerWindowsBrandShortcut(appId, appIconPath) {
if (process.platform !== "win32") return null;
const shortcutPath = windowsBrandShortcutPath();
const shortcutTempPath = `${shortcutPath}.${process.pid}.tmp.lnk`;
await mkdir(path.dirname(shortcutPath), { recursive: true });
// Recreate instead of replacing in place. Explorer can retain the old
// target and search metadata when a prior installer owned this path.
await rm(shortcutPath, { force: true });
await rm(shortcutTempPath, { force: true });
const details = windowsBrandShortcutDetails({
target: windowsExecutablePath(),
appId,
appIconPath,
appName: currentDisplayAppName,
});
const written = writeWindowsBrandShortcut(shell, shortcutTempPath, details, false);
if (!written) throw new Error(`Windows rejected the organization shortcut: ${shortcutPath}`);
await rename(shortcutTempPath, shortcutPath);
if (shell.readShortcutLink(shortcutPath).target !== details.target) {
repairWindowsShortcutTarget(shortcutPath, details);
}
const previousShortcutPath = await readWindowsBrandShortcutMarker();
if (previousShortcutPath && previousShortcutPath !== shortcutPath) {
await rm(previousShortcutPath, { force: true });
}
if (windowsInstalledShortcutPath() !== shortcutPath) {
await rm(windowsInstalledShortcutPath(), { force: true });
}
await writeFile(windowsBrandShortcutMarkerPath(), shortcutPath, "utf8");
return shortcutPath;
}
async function removeWindowsBrandShortcut() {
if (process.platform !== "win32") return;
const shortcutPath = await readWindowsBrandShortcutMarker();
if (shortcutPath) await rm(shortcutPath, { force: true });
await rm(windowsBrandShortcutMarkerPath(), { force: true });
}
function resolveBrandIconImage() {
try {
const cachePath = brandIconCachePath();
if (!existsSync(cachePath)) return null;
const image = nativeImage.createFromPath(cachePath);
return image && !image.isEmpty() ? image : null;
} catch {
return null;
}
}
function brandIconFailure(reason, error) {
const detail = error instanceof Error ? error.message : String(error ?? "");
console.warn(`[brand-icon] ${reason}${detail ? `: ${detail}` : ""}`);
return { ok: false, reason };
}
function recordBrandIconResult(result, sourceUrl) {
if (result.ok) {
brandIconRuntimeState = {
applied: typeof sourceUrl === "string",
sourceUrl: typeof sourceUrl === "string" ? sourceUrl : null,
reason: null,
};
} else {
brandIconRuntimeState = { ...brandIconRuntimeState, reason: result.reason ?? "apply-failed" };
}
return result;
}
async function applyAppIconImage(image, { taskbarIconPath = null, taskbarAppId = APP_IDENTIFIER } = {}) {
if (!image || image.isEmpty()) return brandIconFailure("invalid-image");
try {
if (process.platform === "darwin") {
if (!app.dock) return brandIconFailure("dock-unavailable");
app.dock.setIcon(image);
return { ok: true };
}
if (process.platform === "win32") {
if (!taskbarIconPath || !existsSync(taskbarIconPath)) {
return brandIconFailure("taskbar-icon-missing");
}
if (!mainWindow) return { ok: true };
await applyWindowsTaskbarIcon(mainWindow, {
image,
appId: taskbarAppId,
appIconPath: taskbarIconPath,
relaunchCommand: windowsExecutablePath(),
relaunchDisplayName: currentDisplayAppName,
});
} else {
if (!mainWindow) return brandIconFailure("window-unavailable");
mainWindow.setIcon(image);
}
return { ok: true };
} catch (error) {
return brandIconFailure("os-apply-failed", error);
}
}
async function applyDefaultAppIconImage(expectedSequence = null) {
let image = APP_ICON_IMAGE;
let taskbarIconPath = null;
if (process.platform === "darwin") {
// Packaged macOS builds have no loose default icon file (APP_ICON_IMAGE
// is null), so reset the dock via dock.setIcon(null) to restore the
// bundle icon instead of silently leaving the branded icon in place.
if (expectedSequence !== null && expectedSequence !== brandIconApplySequence) {
return { ok: false, reason: "stale" };
}
try {
const result = resetMacDockIcon(app.dock, image);
return result.ok ? result : brandIconFailure(result.reason);
} catch (error) {
return brandIconFailure("os-apply-failed", error);
}
}
if (process.platform === "win32") {
try {
await removeWindowsBrandShortcut();
app.setAppUserModelId(APP_IDENTIFIER);
} catch (error) {
return brandIconFailure("shortcut-remove-failed", error);
}
if (image && !image.isEmpty()) {
try {
taskbarIconPath = defaultAppWindowsIconPath();
await writeWindowsIconFile(image, taskbarIconPath);
} catch (error) {
return brandIconFailure("stock-icon-unavailable", error);
}
} else {
try {
const executableIcon = await app.getFileIcon(process.execPath, { size: "large" });
if (executableIcon && !executableIcon.isEmpty()) image = executableIcon;
taskbarIconPath = process.execPath;
} catch (error) {
return brandIconFailure("stock-icon-unavailable", error);
}
}
}
if (!image || image.isEmpty()) {
// Linux: the packaged window/launcher icon is managed by the desktop
// integration, so a missing loose icon is a safe no-op.
return process.platform === "win32" ? brandIconFailure("stock-icon-unavailable") : { ok: true };
}
if (process.platform === "win32" && taskbarIconPath) {
try {
await registerWindowsBrandShortcut(APP_IDENTIFIER, taskbarIconPath);
} catch (error) {
return brandIconFailure("shortcut-write-failed", error);
}
}
if (expectedSequence !== null && expectedSequence !== brandIconApplySequence) {
return { ok: false, reason: "stale" };
}
return applyAppIconImage(image, {
taskbarIconPath,
taskbarAppId: APP_IDENTIFIER,
});
}
async function focusMainWindowFromNotification() {
const win = await createMainWindow();
if (win.isDestroyed()) return;
if (win.isMinimized()) win.restore();
win.show();
win.focus();
}
/**
* @param {unknown} input
* @returns {import("@openwork/types/desktop-ipc").DesktopNotificationResult}
*/
function showDesktopNotification(input) {
if (!ElectronNotification.isSupported()) {
return { ok: false, reason: "notifications unsupported" };
}
const record = input && typeof input === "object" ? input : {};
const title = String(Reflect.get(record, "title") ?? "").trim();
if (!title) {
return { ok: false, reason: "missing title" };
}
const body = String(Reflect.get(record, "body") ?? "").trim();
const icon = resolveBrandIconImage() ?? APP_ICON_IMAGE;
const options = {
title,
...(body ? { body } : {}),
...(Reflect.get(record, "silent") === true ? { silent: true } : {}),
...(icon && !icon.isEmpty() ? { icon } : {}),
};
try {
const notification = new ElectronNotification(options);
notification.on("click", () => {
runDetachedTask("focus window from notification", focusMainWindowFromNotification);
});
notification.show();
return { ok: true };
} catch (error) {
const reason = error instanceof Error ? error.message : "failed to show notification";
return { ok: false, reason };
}
}
async function readBrandIconSidecar() {
try {
const parsed = JSON.parse(await readFile(brandIconSidecarPath(), "utf8"));
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
function readBrandIconSidecarSourceUrlSync() {
try {
const parsed = JSON.parse(readFileSync(brandIconSidecarPath(), "utf8"));
return parsed && typeof parsed === "object" && typeof parsed.sourceUrl === "string"
? parsed.sourceUrl
: null;
} catch {
return null;
}
}
async function clearBrandIconCache() {
await Promise.all([
rm(brandIconCachePath(), { force: true }),
rm(brandIconSidecarPath(), { force: true }),
rm(brandIconWindowsPath(), { force: true }),
]);
}
function normalizeBrandIconSourceUrl(value) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
try {
const parsed = new URL(trimmed);
return parsed.protocol === "http:" || parsed.protocol === "https:" ? trimmed : null;
} catch {
return null;
}
}
async function fetchBrandIconBuffer(sourceUrl) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), BRAND_ICON_FETCH_TIMEOUT_MS);
try {
const response = await electronNet.fetch(sourceUrl, {
signal: controller.signal,
credentials: "omit",
cache: "no-store",
headers: {
"user-agent": BRAND_ICON_FETCH_USER_AGENT,
accept: "image/*,*/*",
},
});
if (!response.ok) return { ok: false, reason: "http-status" };
const contentLength = Number(response.headers.get("content-length") ?? "0");
if (Number.isFinite(contentLength) && contentLength > BRAND_ICON_MAX_BYTES) {
return { ok: false, reason: "too-large" };
}
const buffer = Buffer.from(await response.arrayBuffer());
if (buffer.byteLength > BRAND_ICON_MAX_BYTES) {
return { ok: false, reason: "too-large" };
}
return { ok: true, buffer };
} catch (error) {
return { ok: false, reason: error?.name === "AbortError" ? "timeout" : "fetch-failed" };
} finally {
clearTimeout(timeout);
}
}
function brandIconImageRejectionReason(image) {
if (!image || image.isEmpty()) return "invalid-image";
const size = image.getSize();
if (size.width < 64 || size.height < 64) return "too-small";
const aspectRatio = size.width / size.height;
if (aspectRatio < 1 / 1.5 || aspectRatio > 1.5) return "invalid-aspect";
return null;
}
async function writeBrandIconCache(image, sourceUrl) {
const cachePath = brandIconCachePath();
const sidecarPath = brandIconSidecarPath();
const windowsPath = brandIconWindowsPath();
const suffix = `${process.pid}-${Date.now()}`;
const cacheTempPath = `${cachePath}.${suffix}.tmp`;
const sidecarTempPath = `${sidecarPath}.${suffix}.tmp`;
const windowsTempPath = `${windowsPath}.${suffix}.tmp`;
const windowsIcon = process.platform === "win32" ? windowsIconFromNativeImage(image) : null;
try {
await mkdir(path.dirname(cachePath), { recursive: true });
await writeFile(cacheTempPath, image.toPNG());
if (windowsIcon) await writeFile(windowsTempPath, windowsIcon);
await writeFile(sidecarTempPath, JSON.stringify({
sourceUrl,
appliedAt: new Date().toISOString(),
appVersion: app.getVersion(),
}, null, 2), "utf8");
await rename(cacheTempPath, cachePath);
if (windowsIcon) await rename(windowsTempPath, windowsPath);
await rename(sidecarTempPath, sidecarPath);
} catch (error) {
await Promise.all([
rm(cacheTempPath, { force: true }),
rm(sidecarTempPath, { force: true }),
rm(windowsTempPath, { force: true }),
]).catch(() => undefined);
throw error;
}
}
async function writeWindowsIconFile(image, destination) {
const tempPath = `${destination}.${process.pid}-${Date.now()}.tmp`;
try {
await mkdir(path.dirname(destination), { recursive: true });
await writeFile(tempPath, windowsIconFromNativeImage(image));
await rename(tempPath, destination);
} catch (error) {
await rm(tempPath, { force: true }).catch(() => undefined);
throw error;
}
}
async function ensureWindowsBrandIcon(image) {
if (process.platform !== "win32") return null;
const windowsPath = brandIconWindowsPath();
if (!existsSync(windowsPath)) await writeWindowsIconFile(image, windowsPath);
return windowsPath;
}
async function registerWindowsDisplayShortcut() {
if (process.platform !== "win32") return;
const sidecar = await readBrandIconSidecar();
const sourceUrl = typeof sidecar?.sourceUrl === "string" ? sidecar.sourceUrl : null;
const brandedImage = sourceUrl ? resolveBrandIconImage() : null;
if (brandedImage && sourceUrl) {
const iconPath = await ensureWindowsBrandIcon(brandedImage);
await registerWindowsBrandShortcut(windowsBrandAppUserModelId(APP_IDENTIFIER, sourceUrl), iconPath);
return;
}
const stockImage = APP_ICON_IMAGE ?? await app.getFileIcon(windowsExecutablePath(), { size: "large" });
const iconPath = defaultAppWindowsIconPath();
await writeWindowsIconFile(stockImage, iconPath);
await registerWindowsBrandShortcut(APP_IDENTIFIER, iconPath);
}
async function applyCachedBrandIcon(image, sourceUrl, expectedSequence = null) {
let taskbarIconPath = null;
let taskbarAppId = APP_IDENTIFIER;
try {
taskbarIconPath = await ensureWindowsBrandIcon(image);
if (process.platform === "win32") {
taskbarAppId = windowsBrandAppUserModelId(APP_IDENTIFIER, sourceUrl);
await registerWindowsBrandShortcut(taskbarAppId, taskbarIconPath);
app.setAppUserModelId(taskbarAppId);
}
} catch (error) {
if (expectedSequence !== null && expectedSequence !== brandIconApplySequence) {
return { ok: false, reason: "stale" };
}
return recordBrandIconResult(brandIconFailure("write-failed", error), sourceUrl);
}
if (expectedSequence !== null && expectedSequence !== brandIconApplySequence) {
return { ok: false, reason: "stale" };
}
return recordBrandIconResult(await applyAppIconImage(image, {
taskbarIconPath,
taskbarAppId,
}), sourceUrl);
}
async function applyBrandIconUrl(value) {
const sequence = ++brandIconApplySequence;
if (value === null) {
const result = await applyDefaultAppIconImage(sequence);
if (result.reason === "stale") return result;
const applied = recordBrandIconResult(result, null);
if (!applied.ok) return applied;
try {
await clearBrandIconCache();
return applied;
} catch (error) {
return recordBrandIconResult(brandIconFailure("clear-failed", error), null);
}
}
const sourceUrl = normalizeBrandIconSourceUrl(value);
if (!sourceUrl) return recordBrandIconResult(brandIconFailure("invalid-url"), null);
const sidecar = await readBrandIconSidecar();
const cachedImage = resolveBrandIconImage();
if (sidecar?.sourceUrl === sourceUrl && cachedImage) {
return applyCachedBrandIcon(cachedImage, sourceUrl, sequence);
}
const fetched = await fetchBrandIconBuffer(sourceUrl);
if (sequence !== brandIconApplySequence) return { ok: false, reason: "stale" };
if (!fetched.ok) return recordBrandIconResult(brandIconFailure(fetched.reason), sourceUrl);
const image = nativeImage.createFromBuffer(fetched.buffer);
const rejectionReason = brandIconImageRejectionReason(image);
if (rejectionReason) return recordBrandIconResult(brandIconFailure(rejectionReason), sourceUrl);
try {
await writeBrandIconCache(image, sourceUrl);
} catch (error) {
if (sequence !== brandIconApplySequence) return { ok: false, reason: "stale" };
return recordBrandIconResult(brandIconFailure("write-failed", error), sourceUrl);
}
if (sequence !== brandIconApplySequence) {
const latestSidecar = await readBrandIconSidecar();
if (latestSidecar?.sourceUrl === sourceUrl) {
await clearBrandIconCache().catch(() => undefined);
}
return { ok: false, reason: "stale" };
}
return applyCachedBrandIcon(image, sourceUrl, sequence);
}
async function getBrandIconState() {
return { ...brandIconRuntimeState };
}
const INITIAL_BRAND_ICON_IMAGE = resolveBrandIconImage();
const INITIAL_APP_ICON_IMAGE = INITIAL_BRAND_ICON_IMAGE ?? APP_ICON_IMAGE;
if (INITIAL_BRAND_ICON_IMAGE) {
// The renderer's level-based reconcile compares getBrandIconState() with
// the fresh org config. Record that a cached brand icon is restored at
// boot so a clear whose edge the renderer missed still resets the icon.
brandIconRuntimeState = {
applied: true,
sourceUrl: readBrandIconSidecarSourceUrlSync(),
reason: null,
};
}
if (process.platform === "darwin" && INITIAL_APP_ICON_IMAGE && !INITIAL_APP_ICON_IMAGE.isEmpty() && app.dock) {
app.dock.setIcon(INITIAL_APP_ICON_IMAGE);
}
// Expose Chrome DevTools Protocol so the opencode-chrome-devtools plugin can
// drive the built-in browser panel. Use OPENWORK_ELECTRON_REMOTE_DEBUG_PORT to
// pin a specific port; otherwise probe for a free one starting at 9223.
// Must resolve before app.commandLine.appendSwitch (before `ready`).
function probePort(port) {
return new Promise((resolve) => {
const srv = net.createServer();
srv.once("error", () => resolve(false));
srv.listen({ port, host: "127.0.0.1" }, () => {
srv.close(() => resolve(true));
});
});
}
async function findFreeCdpPort(candidates) {
for (const port of candidates) {
if (await probePort(port)) return port;
}
return 0;
}
const explicitCdpPort = Number.parseInt(
process.env.OPENWORK_ELECTRON_REMOTE_DEBUG_PORT?.trim() ?? "",
10,
);
const remoteDebugPort = Number.isFinite(explicitCdpPort) && explicitCdpPort > 0
? explicitCdpPort
: await findFreeCdpPort([9223, 9224, 9225, 9226, 9227]);
if (remoteDebugPort > 0) {
app.commandLine.appendSwitch("remote-debugging-port", String(remoteDebugPort));
app.commandLine.appendSwitch("remote-debugging-address", "127.0.0.1");
}
// Make the resolved port available to the embedded server so it flows into
// agent instructions via ensureOpenworkAgent → resolveAgentTemplate.
process.env.OPENWORK_ELECTRON_REMOTE_DEBUG_PORT = String(remoteDebugPort);
if (isDevMode && !app.isPackaged) {
const cdpAddress = remoteDebugPort > 0 ? `http://127.0.0.1:${remoteDebugPort}` : "disabled";
console.log(`[openwork] dev profile=${app.getPath("userData")} cdp=${cdpAddress}`);
}
// Apply extra Chromium flags from ELECTRON_EXTRA_LAUNCH_ARGS.
// Used in headless/Daytona environments to pass e.g. --disable-gpu.
const extraLaunchArgs = (process.env.ELECTRON_EXTRA_LAUNCH_ARGS ?? "").trim();
if (extraLaunchArgs) {
for (const arg of extraLaunchArgs.split(/\s+/)) {
const cleaned = arg.replace(/^--/, "");
if (!cleaned) continue;
const eqIdx = cleaned.indexOf("=");
if (eqIdx > 0) {
app.commandLine.appendSwitch(cleaned.slice(0, eqIdx), cleaned.slice(eqIdx + 1));
} else {
app.commandLine.appendSwitch(cleaned);
}
}
}
configureFakeMediaForTests(app, envFlagEnabled("OPENWORK_ELECTRON_FAKE_MEDIA"));
const DEFAULT_DEN_BASE_URL = "https://app.openworklabs.com";
const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:4096";
const FORCE_DESKTOP_REQUIRE_SIGNIN =
DESKTOP_DISTRIBUTION.requireSignin || envFlagEnabled("OPENWORK_FORCE_SIGNIN");
const DEFAULT_DESKTOP_REQUIRE_SIGNIN = FORCE_DESKTOP_REQUIRE_SIGNIN;
function envFlagEnabled(name) {
const value = process.env[name]?.trim().toLowerCase();
return value === "1" || value === "true" || value === "yes" || value === "on";
}
const IDLE_ENGINE_INFO = Object.freeze({
running: false,
runtime: "direct",
managedByServer: false,
baseUrl: null,
projectDir: null,
hostname: null,
port: null,
opencodeUsername: null,
opencodePassword: null,
opencodeBinPath: null,
opencodeBinSource: null,
pid: null,
lastStdout: null,
lastStderr: null,
});
const IDLE_OPENWORK_SERVER_INFO = Object.freeze({
running: false,
remoteAccessEnabled: false,
host: null,
port: null,
baseUrl: null,
connectUrl: null,
mdnsUrl: null,
lanUrl: null,
clientToken: null,
ownerToken: null,
hostToken: null,
managedOpencodeBinPath: null,
managedOpencodeBinSource: null,
pid: null,
lastStdout: null,
lastStderr: null,
});
const IDLE_ROUTER_INFO = Object.freeze({
running: false,
version: null,
workspacePath: null,
opencodeUrl: null,
healthPort: null,
pid: null,
lastStdout: null,
lastStderr: null,
});
let mainWindow = null;
const pendingDeepLinks = [];
const browserPanel = createBrowserPanel({
remoteDebugPort,
getWindow: () => mainWindow,
onDeepLink: (urls) => queueDeepLinks(urls),
});
const workspaceStore = createWorkspaceStore({
app,
defaultDenBaseUrl: DEFAULT_DEN_BASE_URL,
defaultRequireSignin: DEFAULT_DESKTOP_REQUIRE_SIGNIN,
forceRequireSignin: FORCE_DESKTOP_REQUIRE_SIGNIN,
});
const activeDesktopTransfers = new Map();
function desktopTransferKey(event, transferId) {
const normalizedId = typeof transferId === "string" ? transferId.trim() : "";
if (!normalizedId || normalizedId.length > 128 || !/^[a-zA-Z0-9._-]+$/.test(normalizedId)) {
throw new Error("A valid transferId is required.");
}
return `${event.sender.id}:${normalizedId}`;
}
async function runDesktopTransfer(event, input, operation) {
const key = desktopTransferKey(event, input?.transferId);
if (activeDesktopTransfers.has(key)) throw new Error("transferId is already active.");
const controller = new AbortController();
const abort = () => controller.abort();
activeDesktopTransfers.set(key, controller);
event.sender.once("destroyed", abort);
try {
// Both authorities come from app-owned state in userData; workspace-
// writable configuration must never widen where a transfer may write.
const [authorizedRoots, allowedUrlPrefixes] = await Promise.all([
workspaceStore.listLocalWorkspacePaths(),
workspaceStore.listRemoteWorkspaceUrlPrefixes(),
]);
return await operation(input, {
authorizedRoots,
allowedUrlPrefixes,
// App-owned staging keeps in-flight downloads outside every authorized
// workspace root until they complete.
stagingDir: path.join(app.getPath("userData"), "binary-transfers"),
fetcher: electronNet.fetch,
signal: controller.signal,
});
} finally {
event.sender.removeListener("destroyed", abort);
activeDesktopTransfers.delete(key);
}
}
const connectLinkReplayGuard = createConnectLinkReplayGuard({
filePath: path.join(app.getPath("userData"), "connect-link-seen.json"),
});
/**
* @param {string} rawUrl
* @returns {import("@openwork/types/connect-link").ConnectLinkVerifyResult}
*/
function verifyConnectLink(rawUrl) {
return verifyConnectLinkUrl(String(rawUrl ?? ""), {
publicKeys: resolveConnectLinkPublicKeys(),
// http is refused everywhere except loopback targets in dev runs.
allowInsecureLoopback: isDevMode,
});
}
async function previewConnectLink(rawUrl) {
if (extractConnectExchange(rawUrl)) {
return resolveConnectExchangeUrl(rawUrl, {
mode: "preview",
fetcher: electronNet.fetch,
allowInsecureLoopback: isDevMode,
});
}
return verifyConnectLink(rawUrl);
}
async function acceptConnectLink(rawUrl) {
if (extractConnectExchange(rawUrl)) {
return resolveConnectExchangeUrl(rawUrl, {
mode: "exchange",
fetcher: electronNet.fetch,
allowInsecureLoopback: isDevMode,
});
}
return verifyConnectLink(rawUrl);
}
async function persistConnectLinkClaims(claims) {
const previous = workspaceStore.readDesktopBootstrapConfigSync();
const config = await persistConnectLinkBranding(claims, {
persistBootstrap: (config) => workspaceStore.setDesktopBootstrapConfig(config),
applyBrandIconUrl: (iconUrl) => applyBrandIconUrl(iconUrl).catch((error) =>
brandIconFailure("connect-apply-failed", error)),
enterpriseActivation: DESKTOP_DISTRIBUTION.flavor === "enterprise"
? {
activatedAt: new Date().toISOString(),
denBaseUrl: claims.den.baseUrl,
}
: null,
});
if (
desktopActivationRequired(DESKTOP_DISTRIBUTION, previous)
&& !desktopActivationRequired(DESKTOP_DISTRIBUTION, config)
) {
await uiControlServer.start().catch((error) => {
console.warn("[ui-control] failed to start", error);
});
await runtimeManager.prepareFreshRuntime();
}
return config;
}
function normalizePlatform(value) {
if (value === "darwin" || value === "linux") return value;
if (value === "win32") return "windows";
return "linux";
}
function forwardedDeepLinks(argv) {
return argv
.slice(1)
.map((entry) => entry.trim())
.filter(
(entry) =>
entry.startsWith(`${DESKTOP_PROTOCOL_SCHEME}://`) ||
(!app.isPackaged && entry.startsWith("openwork-dev://")) ||
entry.startsWith("https://") ||
entry.startsWith("http://"),
);
}
function queueDeepLinks(urls) {
const nextUrls = urls.filter(Boolean);
if (nextUrls.length === 0) return;
pendingDeepLinks.push(...nextUrls);
if (mainWindow?.webContents) {
mainWindow.webContents.send(NATIVE_DEEP_LINK_EVENT, nextUrls);
}
}
function flushPendingDeepLinks() {
if (!mainWindow?.webContents || pendingDeepLinks.length === 0) return;
const urls = pendingDeepLinks.splice(0, pendingDeepLinks.length);
mainWindow.webContents.send(NATIVE_DEEP_LINK_EVENT, urls);
}
function globalOpencodeRoot() {
return globalOpencodeConfigDir();
}
function execResult(ok, stdout = "", stderr = "", status = ok ? 0 : 1) {
return { ok, status, stdout, stderr };
}
async function pathExists(targetPath) {
try {
await stat(targetPath);
return true;
} catch {
return false;
}
}
async function isDirectory(targetPath) {
try {
return (await stat(targetPath)).isDirectory();
} catch {
return false;
}
}
function sanitizeCommandName(raw) {
const trimmed = String(raw ?? "").trim().replace(/^\/+/, "");
if (!trimmed) return null;
const safe = Array.from(trimmed)
.filter((char) => /[A-Za-z0-9_-]/.test(char))
.join("");
return safe || null;
}
function escapeYamlScalar(value) {
return JSON.stringify(String(value ?? ""));
}
function serializeCommandFrontmatter(command) {
const template = String(command?.template ?? "").trim();
if (!template) {
throw new Error("command.template is required");
}
let output = "---\n";
if (typeof command?.description === "string" && command.description.trim()) {
output += `description: ${escapeYamlScalar(command.description.trim())}\n`;
}
if (typeof command?.agent === "string" && command.agent.trim()) {
output += `agent: ${escapeYamlScalar(command.agent.trim())}\n`;
}
if (typeof command?.model === "string" && command.model.trim()) {
output += `model: ${escapeYamlScalar(command.model.trim())}\n`;
}
if (command?.subtask === true) {
output += "subtask: true\n";
}
output += `---\n\n${template}\n`;
return output;
}
function validateSkillName(raw) {
const trimmed = String(raw ?? "").trim();
if (!trimmed || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(trimmed)) {
throw new Error("skill name must be kebab-case");
}
return trimmed;
}
const runtimeManager = createRuntimeManager({
app,
desktopRoot: path.resolve(__dirname, ".."),
listLocalWorkspacePaths: () => workspaceStore.listLocalWorkspacePaths(),
// When OPENWORK_ENCRYPTION_KEY is set, skip the safeStorage provider so it does not shadow the documented env override used by CI/headless/enterprise.
localManagedMcpVaultKey: process.env.OPENWORK_ENCRYPTION_KEY?.trim()
? undefined
: createDesktopVaultKeyProvider({
filePath: path.join(app.getPath("userData"), "local-managed-mcp-vault-key.bin"),
loadSafeStorage: () => require("electron").safeStorage,
}),
});
const initialRunnerBootstrap = workspaceStore.readDesktopBootstrapConfigSync();
const legacyRunnerBaseUrls = [
initialRunnerBootstrap.apiBaseUrl,
initialRunnerBootstrap.baseUrl,
initialRunnerBootstrap.baseUrl
? `${String(initialRunnerBootstrap.baseUrl).replace(/\/+$/, "")}/api/den`
: null,
`${DEFAULT_DEN_BASE_URL}/api/den`,
].map((value) => normalizeRunnerBaseUrl(value)).filter(Boolean);
const desktopAutomationRunner = createDesktopAutomationRunner({
// v1 credentials predate token audiences. Keep them usable during the Den
// rollout only for endpoints trusted before the renderer starts issuing IPC.
legacyBaseUrls: legacyRunnerBaseUrls,
getLocalRuntime: async () => {
const server = await runtimeManager.openworkServerInfo();
return { baseUrl: server.baseUrl, token: server.clientToken ?? server.ownerToken };
},
log: (state) => console.info(`[automation-runner] ${state}`),
onCredentialRejected: () => {
if (!mainWindow || mainWindow.isDestroyed()) return;
mainWindow.webContents.send(AUTOMATION_RUNNER_CREDENTIAL_REJECTED_EVENT);
},
});
// Scheduled Automations are due at wall-clock times a laptop routinely sleeps
// through. Waking the machine has to poll for work now, not up to a full poll
// interval later, or a recovered occurrence sits queued while the desktop is
// already back.
const wakeAutomationRunner = (wakeEvent) => {
if (desktopAutomationRunner.wake().polled) {
console.info(`[automation-runner] polling for work after ${wakeEvent}`);
}
};
powerMonitor.on("resume", () => wakeAutomationRunner("resume"));
powerMonitor.on("unlock-screen", () => wakeAutomationRunner("unlock-screen"));
let runtimeDisposedForQuit = false;
let runtimeDisposeInProgress = false;
let runtimeBootstrapPromise = null;
function showShutdownScreen() {
const win = mainWindow;
if (!win || win.isDestroyed()) return;
try {
win.show();
void win.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
html, body { height: 100%; margin: 0; background: #0b0b0f; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
body { display: grid; place-items: center; }
main { display: grid; gap: 10px; justify-items: center; }
.spinner { width: 22px; height: 22px; border: 2px solid rgba(244,244,245,.25); border-top-color: #f4f4f5; border-radius: 50%; animation: spin .9s linear infinite; }
.title { font-size: 15px; font-weight: 600; }
.body { font-size: 13px; color: #a1a1aa; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<main>
<div class="spinner" aria-hidden="true"></div>
<div class="title">Stopping OpenWork services</div>
<div class="body">Closing local workers and background services...</div>
</main>
</body>
</html>`)}`).catch(() => undefined);
} catch {
// Ignore renderer teardown races during quit.
}
}
async function disposeRuntimeBeforeQuit() {
if (runtimeDisposedForQuit || runtimeDisposeInProgress) return;
runtimeDisposeInProgress = true;
try {
await runtimeManager.dispose().catch(() => undefined);
runtimeDisposedForQuit = true;
} finally {
runtimeDisposeInProgress = false;
}
}
function assertOpenworkServerReady(info) {
if (!info?.running) {
throw new Error("OpenWork server did not stay running after startup.");
}
if (!info.baseUrl) {
throw new Error("OpenWork server did not report a base URL after startup.");
}
if (!info.ownerToken && !info.clientToken) {
throw new Error("OpenWork server did not report an access token after startup.");
}
return info;
}
async function bootRuntimeForSelectedWorkspace() {
if (typeof process.env.OPENWORK_EVAL_FATAL_DESKTOP_BOOTSTRAP_FAILURE === "string") {
throw new Error(process.env.OPENWORK_EVAL_FATAL_DESKTOP_BOOTSTRAP_FAILURE);
}
const list = await workspaceStore.readWorkspaceState();
const selectedId = list.selectedId || list.activeId || list.workspaces[0]?.id || "";
const workspace = selectedId
? list.workspaces.find((entry) => entry?.id === selectedId)
: list.workspaces[0];
const workspaceRoot = String(workspace?.path ?? "").trim();
if (!workspaceRoot || workspace?.workspaceType === "remote") {
return { ok: true, skipped: true, reason: "no-local-workspace" };
}
const workspacePaths = [];
for (const entry of list.workspaces) {
if (entry?.workspaceType === "remote") continue;
const workspacePath = String(entry?.path ?? "").trim();
if (workspacePath && !workspacePaths.includes(workspacePath)) workspacePaths.push(workspacePath);
}
if (!workspacePaths.includes(workspaceRoot)) workspacePaths.unshift(workspaceRoot);
let bootWorkspace = workspace;
let bootWorkspaceRoot = workspaceRoot;
let engine;
try {
engine = await runtimeManager.engineStart(workspaceRoot, {
runtime: "direct",
workspacePaths,
});
} catch (error) {
const fallback = list.workspaces.find((entry) => {
const candidatePath = String(entry?.path ?? "").trim();
return entry?.workspaceType !== "remote" && candidatePath && candidatePath !== workspaceRoot;
});
const fallbackRoot = String(fallback?.path ?? "").trim();
if (!fallback || !fallbackRoot) throw error;
console.warn("[runtime] selected workspace failed during boot; trying fallback workspace", {
selectedWorkspaceId: workspace?.id ?? null,
fallbackWorkspaceId: fallback.id ?? null,
error: error instanceof Error ? error.message : String(error),
});
const fallbackWorkspacePaths = [
fallbackRoot,
...workspacePaths.filter((entry) => entry !== fallbackRoot && entry !== workspaceRoot),
];
engine = await runtimeManager.engineStart(fallbackRoot, {
runtime: "direct",
workspacePaths: fallbackWorkspacePaths,
});
bootWorkspace = fallback;
bootWorkspaceRoot = fallbackRoot;
await workspaceStore.writeWorkspaceState({
...list,
selectedId: String(fallback.id ?? ""),
watchedId: String(fallback.id ?? ""),
}).catch(() => undefined);
}
const openworkServer = assertOpenworkServerReady(await runtimeManager.openworkServerInfo());
return { ok: true, skipped: false, engine, openworkServer, workspaceId: bootWorkspace.id ?? null };
}
function ensureRuntimeBootstrap() {
if (!runtimeBootstrapPromise) {
runtimeBootstrapPromise = bootRuntimeForSelectedWorkspace().catch((error) => ({
ok: false,
error: error instanceof Error ? error.message : String(error),
}));
}
return runtimeBootstrapPromise;
}
function resolveOpencodeConfigPath(scope, projectDir) {
if (scope === "project") {
if (!String(projectDir ?? "").trim()) {
throw new Error("projectDir is required");
}
return workspaceOpencodeConfigCandidates(projectDir);
} else if (scope === "global") {
const root = globalOpencodeRoot();
return [path.join(root, "opencode.jsonc"), path.join(root, "opencode.json")];
} else {
throw new Error("scope must be 'project' or 'global'");
}
}
async function selectOpencodeConfigPath(candidates) {
for (const candidate of candidates) {
if (await pathExists(candidate)) return candidate;
}
return candidates[0];
}
async function readOpencodeConfig(scope, projectDir) {
const chosenPath = await selectOpencodeConfigPath(resolveOpencodeConfigPath(scope, projectDir));
const exists = await pathExists(chosenPath);
return {
path: chosenPath,
exists,
content: exists ? await readFile(chosenPath, "utf8") : null,
};
}
async function writeOpencodeConfig(scope, projectDir, content) {
const targetPath = await selectOpencodeConfigPath(resolveOpencodeConfigPath(scope, projectDir));
await mkdir(path.dirname(targetPath), { recursive: true });
await writeFile(targetPath, content, "utf8");
return execResult(true, `Wrote ${targetPath}`);
}
function resolveCommandsDir(scope, projectDir) {
if (scope === "workspace") {
if (!String(projectDir ?? "").trim()) {
throw new Error("projectDir is required");
}
return path.join(projectDir, ".opencode", "commands");
}
if (scope === "global") {
return path.join(globalOpencodeRoot(), "commands");
}
throw new Error("scope must be 'workspace' or 'global'");
}
async function listCommandNames(scope, projectDir) {
const commandsDir = resolveCommandsDir(scope, projectDir);
if (!(await isDirectory(commandsDir))) {
return [];
}
const entries = await readdir(commandsDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
.map((entry) => entry.name.replace(/\.md$/, ""))
.sort();
}
async function writeCommandFile(scope, projectDir, command) {
const safeName = sanitizeCommandName(command?.name);
if (!safeName) {
throw new Error("command.name is required");
}
const commandsDir = resolveCommandsDir(scope, projectDir);
await mkdir(commandsDir, { recursive: true });
const filePath = path.join(commandsDir, `${safeName}.md`);
await writeFile(filePath, serializeCommandFrontmatter({ ...command, name: safeName }), "utf8");
return execResult(true, `Wrote ${filePath}`);
}
async function deleteCommandFile(scope, projectDir, name) {
const safeName = sanitizeCommandName(name);
if (!safeName) {
throw new Error("name is required");
}
const commandsDir = resolveCommandsDir(scope, projectDir);
const filePath = path.join(commandsDir, `${safeName}.md`);
if (await pathExists(filePath)) {
await rm(filePath, { force: true });
}
return execResult(true, `Deleted ${filePath}`);
}
async function collectProjectSkillRoots(projectDir) {
const roots = [];
let current = path.resolve(projectDir);
while (true) {
const opencodeSkills = path.join(current, ".opencode", "skills");
const legacySkills = path.join(current, ".opencode", "skill");
const claudeSkills = path.join(current, ".claude", "skills");
if (await isDirectory(opencodeSkills)) roots.push(opencodeSkills);
if (await isDirectory(legacySkills)) roots.push(legacySkills);
if (await isDirectory(claudeSkills)) roots.push(claudeSkills);
if (await pathExists(path.join(current, ".git"))) {
break;
}
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
}
return roots;
}
async function collectGlobalSkillRoots() {
const roots = [];
const candidates = [
path.join(globalOpencodeRoot(), "skills"),
path.join(os.homedir(), ".claude", "skills"),
path.join(os.homedir(), ".agents", "skills"),
path.join(os.homedir(), ".agent", "skills"),
];
for (const candidate of candidates) {
if (await isDirectory(candidate)) {
roots.push(candidate);
}
}
return roots;
}
async function collectSkillRoots(projectDir) {
const roots = [...(await collectProjectSkillRoots(projectDir)), ...(await collectGlobalSkillRoots())];
return roots.filter((value, index) => roots.indexOf(value) === index);
}
async function findSkillDirsInRoot(root) {
const found = [];
if (!(await isDirectory(root))) return found;
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const direct = path.join(root, entry.name);
if (await pathExists(path.join(direct, "SKILL.md"))) {
found.push(direct);
continue;
}
const nestedEntries = await readdir(direct, { withFileTypes: true }).catch(() => []);
for (const nested of nestedEntries) {
if (!nested.isDirectory()) continue;
const nestedDir = path.join(direct, nested.name);
if (await pathExists(path.join(nestedDir, "SKILL.md"))) {
found.push(nestedDir);
}
}
}
return found;
}
function extractFrontmatterValue(raw, keys) {
const match = raw.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
for (const line of match[1].split(/\r?\n/)) {
const separator = line.indexOf(":");
if (separator <= 0) continue;
const key = line.slice(0, separator).trim().toLowerCase();
if (!keys.includes(key)) continue;
const value = line.slice(separator + 1).trim().replace(/^['"]|['"]$/g, "");
if (value) return value;
}
return null;
}
function extractTrigger(raw) {
return extractFrontmatterValue(raw, ["trigger", "when"]);
}
function extractDescription(raw) {
let inFrontmatter = false;
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
if (trimmed === "---") {
inFrontmatter = !inFrontmatter;
continue;
}
if (inFrontmatter || trimmed.startsWith("#")) continue;
const cleaned = trimmed.replace(/`/g, "");
return cleaned.length > 180 ? `${cleaned.slice(0, 180)}...` : cleaned;
}
return null;
}
async function listLocalSkills(projectDir) {
if (!String(projectDir ?? "").trim()) {
throw new Error("projectDir is required");
}
const seen = new Set();
const out = [];
for (const root of await collectSkillRoots(projectDir)) {
for (const skillDir of await findSkillDirsInRoot(root)) {
const name = path.basename(skillDir);
if (seen.has(name)) continue;
seen.add(name);
let raw = "";
try {
raw = await readFile(path.join(skillDir, "SKILL.md"), "utf8");
} catch {
raw = "";
}
out.push({
name,
path: skillDir,
description: extractDescription(raw) ?? undefined,
trigger: extractTrigger(raw) ?? undefined,
});
}
}
return out.sort((a, b) => a.name.localeCompare(b.name));
}
async function findSkillFile(projectDir, name) {
const safeName = validateSkillName(name);
for (const root of await collectSkillRoots(projectDir)) {
const direct = path.join(root, safeName, "SKILL.md");
if (await pathExists(direct)) return direct;
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const nested = path.join(root, entry.name, safeName, "SKILL.md");
if (await pathExists(nested)) return nested;
}
}
return null;
}
async function ensureProjectSkillRoot(projectDir) {
if (!String(projectDir ?? "").trim()) {
throw new Error("projectDir is required");
}
const opencodeRoot = path.join(projectDir, ".opencode");
const legacy = path.join(opencodeRoot, "skill");
const modern = path.join(opencodeRoot, "skills");
if ((await isDirectory(legacy)) && !(await pathExists(modern))) {
await rename(legacy, modern);
}
await mkdir(modern, { recursive: true });
return modern;
}
function engineDoctor(options = {}) {
return runtimeManager.engineDoctor(options);
}
function activeWindowFromEvent(event) {
return BrowserWindow.fromWebContents(event.sender) ?? mainWindow ?? undefined;
}
function macosVibrancyForCurrentTheme() {
return nativeTheme.shouldUseDarkColors ? "under-window" : "sidebar";
}
function applyNativeTheme(mode) {
nativeTheme.themeSource = mode;
if (process.platform !== "darwin") {
return true;
}
mainWindow?.setVibrancy(macosVibrancyForCurrentTheme());
mainWindow?.setBackgroundColor("#00000001");
return true;
}
// Desktop IPC command registry. Every command invokable from the renderer's
// desktopBridge Proxy (apps/app/src/app/lib/desktop.ts) has exactly one
// entry here; handlers receive the ipcMain event followed by the renderer
// arguments. The @type below asserts this registry against the shared
// DesktopCommandMap contract (packages/types/src/desktop-ipc.ts): a missing,
// extra, or renamed command fails `pnpm --filter @openwork/desktop
// typecheck:electron`.
/** @type {import("@openwork/types/desktop-ipc").DesktopCommandHandlers<import("electron").IpcMainInvokeEvent>} */
const desktopCommandHandlers = {
"workspaceBootstrap": async (event, ...args) => {
return workspaceStore.readWorkspaceState();
},
"workspaceSetSelected": async (event, ...args) => {
return workspaceStore.setSelectedWorkspace(typeof args[0] === "string" ? args[0] : "");
},
"workspaceSetRuntimeActive": async (event, ...args) => {
return workspaceStore.setRuntimeActiveWorkspace(typeof args[0] === "string" && args[0].trim() ? args[0] : null);
},
"workspaceCreate": async (event, ...args) => {
return workspaceStore.createWorkspace(args[0] ?? {});
},
"workspaceCreateRemote": async (event, ...args) => {
return workspaceStore.createRemoteWorkspace(args[0] ?? {});
},
"workspaceUpdateRemote": async (event, ...args) => {
return workspaceStore.updateRemoteWorkspace(args[0] ?? {});
},
"workspaceUpdateDisplayName": async (event, ...args) => {
return workspaceStore.updateWorkspaceDisplayName(args[0] ?? {});
},
"workspaceForget": async (event, ...args) => {
return workspaceStore.forgetWorkspace(String(args[0] ?? "").trim());
},
"workspaceAddAuthorizedRoot": async (event, ...args) => {
return workspaceStore.addAuthorizedRoot(args[0] ?? {});
},
"workspaceOpenworkRead": async (event, ...args) => {
return workspaceStore.readWorkspaceOpenworkConfig(String(args[0]?.workspacePath ?? "").trim());
},
"workspaceOpenworkWrite": async (event, ...args) => {
return workspaceStore.writeWorkspaceOpenworkConfig(
String(args[0]?.workspacePath ?? "").trim(),
args[0]?.config ?? workspaceStore.defaultWorkspaceOpenworkConfig(""),
);
},
"workspaceExportConfig": async (event, ...args) => {
return workspaceStore.exportConfig(args[0] ?? {});
},
"workspaceImportConfig": async (event, ...args) => {
return workspaceStore.importConfig(args[0] ?? {});
},
"opencodeCommandList": async (event, ...args) => {
return listCommandNames(String(args[0]?.scope ?? "").trim(), String(args[0]?.projectDir ?? "").trim());
},
"opencodeCommandWrite": async (event, ...args) => {
return writeCommandFile(
String(args[0]?.scope ?? "").trim(),
String(args[0]?.projectDir ?? "").trim(),
args[0]?.command ?? {},
);
},
"opencodeCommandDelete": async (event, ...args) => {
return deleteCommandFile(
String(args[0]?.scope ?? "").trim(),
String(args[0]?.projectDir ?? "").trim(),
String(args[0]?.name ?? "").trim(),
);
},
"engineStart": async (event, ...args) => {
const projectDir = String(args[0] ?? "").trim();
const options = args[1] ?? {};
return runtimeManager.engineStart(projectDir, options);
},
"prepareFreshRuntime": async (event, ...args) => {
return runtimeManager.prepareFreshRuntime();
},
"runtimeBootstrap": async (event, ...args) => {
return ensureRuntimeBootstrap();
},
"runtimeStatus": async (event, ...args) => {
return runtimeManager.runtimeStatus();
},
"engineStop": async (event, ...args) => {
return runtimeManager.engineStop();
},
"engineRestart": async (event, ...args) => {
return runtimeManager.engineRestart(args[0] ?? {});
},
"engineInfo": async (event, ...args) => {
return runtimeManager.engineInfo();
},
"engineDoctor": async (event, ...args) => {
return engineDoctor(args[0]);
},
"engineInstall": async (event, ...args) => {
return runtimeManager.engineInstall();
},
"appBuildInfo": async (event, ...args) => {
return {
version: app.getVersion(),
gitSha: process.env.OPENWORK_GIT_SHA ?? null,
buildEpoch: process.env.OPENWORK_BUILD_EPOCH ?? null,
openworkDevMode: process.env.OPENWORK_DEV_MODE === "1",
};
},
"desktopNotificationShow": async (event, ...args) => {
return showDesktopNotification(args[0] ?? {});
},
"desktopSentrySetSession": async (event, ...args) => {
const input = args[0] ?? {};
return {
enabled: setOpenworkSentrySession({
userId: input.userId,
orgId: input.orgId,
}),
};
},
"desktopSentryClearSession": async (event, ...args) => {
return { enabled: clearOpenworkSentrySession() };
},
"desktopIntegrationStatus": async (event, ...args) => {
return linuxDesktopIntegration.getStatus();
},
"desktopIntegrationInstall": async (event, ...args) => {
if (BLANK_SLATE_LAUNCH.enabled) throw new Error("Desktop integration is disabled for test profiles.");
return linuxDesktopIntegration.install(args[0] ?? {});
},
"desktopIntegrationRemove": async (event, ...args) => {
if (BLANK_SLATE_LAUNCH.enabled) throw new Error("Desktop integration is disabled for test profiles.");
return linuxDesktopIntegration.remove();
},
"getUiControlBridgeInfo": async (event, ...args) => {
try {
const raw = await readFile(path.join(app.getPath("userData"), "openwork-ui-control.json"), "utf8");
return JSON.parse(raw);
} catch {
return null;
}
},
"getOpenworkUiMcpCommand": async (event, ...args) => {
if (process.env.OPENWORK_DEV_MODE === "1") {
return ["node", path.resolve(__dirname, "../../..", "packages/openwork-ui-mcp/index.mjs")];
}
return ["npx", "-y", "openwork-ui-mcp"];
},
"getComputerUseMcpCommand": async (event, ...args) => {
return getComputerUseMcpCommand();
},
"checkComputerUsePermissions": async (event, ...args) => {
// Spawn --check → fresh TCC read → always accurate.
return checkComputerUsePermissions();
},
"listRunningApps": async (event, ...args) => {
// Running regular macOS apps for composer @App mentions.
return listRunningApps();
},
"openComputerUsePermissionSetup": async (event, ...args) => {
// Open the GUI app. Returns immediately — React shows "verify" CTA.
await openComputerUseSetupApp();
// Return a fresh check so the UI shows the current state.
return checkComputerUsePermissions();
},
"openComputerUsePermissionSettings": async (event, ...args) => {
// Legacy: open the setup app (same as above).
await openComputerUseSetupApp();
return checkComputerUsePermissions();
},
"getOpenworkUiMcpEnvironment": async (event, ...args) => {
return {
OPENWORK_UI_CONTROL_DISCOVERY: path.join(app.getPath("userData"), "openwork-ui-control.json"),
};
},
"getDesktopBootstrapConfig": async (event, ...args) => {
return workspaceStore.getDesktopBootstrapConfig();
},
"debugDesktopBootstrapConfig": async (event, ...args) => {
return workspaceStore.debugDesktopBootstrapConfig();
},
"clearDesktopBootstrapConfig": async (event, ...args) => {
return workspaceStore.clearDesktopBootstrapConfig();
},
"setDesktopBootstrapConfig": async (event, ...args) => {
const previous = workspaceStore.readDesktopBootstrapConfigSync();
// A locked installation must never be able to unlock itself by writing
// policy through the bridge. The activation requirement is cleared only
// by an administrator editing desktop-bootstrap.json on disk (which this
// process reads, never writes) or by a completed Den activation.
const requested = args[0] ?? {};
const guarded = desktopActivationRequired(DESKTOP_DISTRIBUTION, previous)
&& requested?.requireActivation === false
? { ...requested, requireActivation: true }
: requested;
const next = await workspaceStore.setDesktopBootstrapConfig(guarded);
if (
desktopActivationRequired(DESKTOP_DISTRIBUTION, previous)
&& !desktopActivationRequired(DESKTOP_DISTRIBUTION, next)
) {
await uiControlServer.start().catch((error) => {
console.warn("[ui-control] failed to start", error);
});
await runtimeManager.prepareFreshRuntime();
}
return next;
},
"connectLinkVerify": async (event, ...args) => {
// Read-only check — parses + verifies the deep link, writes nothing.
// Replay is surfaced here too so an already-used link gets its refusal
// before the user is ever shown a confirmation.
const verified = await previewConnectLink(String(args[0] ?? ""));
if (verified.ok === false) return verified;
if (verified.transport === "signed" && await connectLinkReplayGuard.has(verified.claims.jti)) {
return { ok: false, code: "replayed", message: "This connect link was already used on this machine." };
}
return verified;
},
"connectLinkAccept": async (event, ...args) => {
// The renderer passes the raw URL back after the user confirmed; claims
// shaped in the renderer are never trusted (desktop-ipc trust boundary).
const verified = await acceptConnectLink(String(args[0] ?? ""));
if (verified.ok === false) return verified;
if (verified.transport === "exchange") {
const config = await persistConnectLinkClaims(verified.claims);
return { ok: true, config };
}
if (await connectLinkReplayGuard.has(verified.claims.jti)) {
return { ok: false, code: "replayed", message: "This connect link was already used on this machine." };
}
// Consume before mutation. If the replay ledger cannot be persisted,
// fail closed and leave the existing bootstrap untouched.
if (!(await connectLinkReplayGuard.remember(verified.claims.jti))) {
return { ok: false, code: "replayed", message: "This connect link was already used on this machine." };
}
const config = await persistConnectLinkClaims(verified.claims);
return { ok: true, config };
},
"nukeOpenworkAndOpencodeConfigPreview": async (event, ...args) => {
return buildNukeManifest({
env: process.env,
homedir: os.homedir(),
platform: process.platform,
preserveBootstrap: args[0]?.preserveBootstrap !== false,
userDataPath: app.getPath("userData"),
workspacePaths: await workspaceStore.listLocalWorkspacePaths(),
});
},
"nukeOpenworkAndOpencodeConfigAndExit": async (event, ...args) => {
return executeNukeFreshStart({
app,
session,
runtimeManager,
uiControlServer,
removeWindowsBrandShortcut,
}, {
preserveBootstrap: args[0]?.preserveBootstrap !== false,
input: {
env: process.env,
homedir: os.homedir(),
platform: process.platform,
userDataPath: app.getPath("userData"),
workspacePaths: await workspaceStore.listLocalWorkspacePaths(),
},
});
},
"sandboxCleanupOpenworkContainers": async (event, ...args) => {
return runtimeManager.sandboxCleanupOpenworkContainers();
},
"openworkServerInfo": async (event, ...args) => {
return runtimeManager.openworkServerInfo();
},
"automationRunnerConfigure": async (event, ...args) => {
return desktopAutomationRunner.configure(args[0] ?? null);
},
"openworkServerRestart": async (event, ...args) => {
return runtimeManager.openworkServerRestart(args[0] ?? {});
},
"pickDirectory": async (event, ...args) => {
const options = args[0] ?? {};
/** @type {import("electron").OpenDialogOptions["properties"]} */
const properties = options.multiple
? ["openDirectory", "createDirectory", "multiSelections"]
: ["openDirectory", "createDirectory"];
const result = await dialog.showOpenDialog(activeWindowFromEvent(event), {
title: options.title,
defaultPath: options.defaultPath,
properties,
});
if (result.canceled) return null;
return options.multiple ? result.filePaths : (result.filePaths[0] ?? null);
},
"pickFile": async (event, ...args) => {
const options = args[0] ?? {};
/** @type {import("electron").OpenDialogOptions["properties"]} */
const properties = options.multiple ? ["openFile", "multiSelections"] : ["openFile"];
const result = await dialog.showOpenDialog(activeWindowFromEvent(event), {
title: options.title,
defaultPath: options.defaultPath,
filters: options.filters,
properties,
});
if (result.canceled) return null;
return options.multiple ? result.filePaths : (result.filePaths[0] ?? null);
},
"saveFile": async (event, ...args) => {
const options = args[0] ?? {};
const result = await dialog.showSaveDialog(activeWindowFromEvent(event), {
title: options.title,
defaultPath: options.defaultPath,
filters: options.filters,
});
return result.canceled ? null : (result.filePath ?? null);
},
"importSkill": async (event, ...args) => {
const projectDir = String(args[0] ?? "").trim();
const sourceDir = String(args[1] ?? "").trim();
const overwrite = args[2]?.overwrite === true;
if (!projectDir || !sourceDir) {
throw new Error("projectDir and sourceDir are required");
}
const skillRoot = await ensureProjectSkillRoot(projectDir);
const name = validateSkillName(path.basename(sourceDir));
const destination = path.join(skillRoot, name);
if (await pathExists(destination)) {
if (!overwrite) {
return execResult(false, "", `Skill already exists at ${destination}`);
}
await rm(destination, { recursive: true, force: true });
}
await cp(sourceDir, destination, { recursive: true });
return execResult(true, `Imported skill to ${destination}`);
},
"installSkillTemplate": async (event, ...args) => {
const projectDir = String(args[0] ?? "").trim();
const name = validateSkillName(args[1]);
const content = String(args[2] ?? "");
const overwrite = args[3]?.overwrite === true;
const skillRoot = await ensureProjectSkillRoot(projectDir);
const destination = path.join(skillRoot, name);
if (await pathExists(destination)) {
if (!overwrite) {
return execResult(false, "", `Skill already exists at ${destination}`);
}
await rm(destination, { recursive: true, force: true });
}
await mkdir(destination, { recursive: true });
await writeFile(path.join(destination, "SKILL.md"), content, "utf8");
return execResult(true, `Installed skill to ${destination}`);
},
"listLocalSkills": async (event, ...args) => {
return listLocalSkills(String(args[0] ?? "").trim());
},
"readLocalSkill": async (event, ...args) => {
const projectDir = String(args[0] ?? "").trim();
const skillPath = await findSkillFile(projectDir, args[1]);
if (!skillPath) {
throw new Error("Skill not found");
}
return { path: skillPath, content: await readFile(skillPath, "utf8") };
},
"writeLocalSkill": async (event, ...args) => {
const projectDir = String(args[0] ?? "").trim();
const skillPath = await findSkillFile(projectDir, args[1]);
if (!skillPath) {
return execResult(false, "", "Skill not found");
}
const content = String(args[2] ?? "");
const next = content.endsWith("\n") ? content : `${content}\n`;
await writeFile(skillPath, next, "utf8");
return execResult(true, `Saved skill ${path.basename(path.dirname(skillPath))}`);
},
"uninstallSkill": async (event, ...args) => {
const projectDir = String(args[0] ?? "").trim();
const skillPath = await findSkillFile(projectDir, args[1]);
if (!skillPath) {
return execResult(false, "", "Skill not found in .opencode/skills or .claude/skills");
}
await rm(path.dirname(skillPath), { recursive: true, force: true });
return execResult(true, `Removed skill ${args[1]}`);
},
"updaterEnvironment": async (event, ...args) => {
const executablePath = app.isPackaged ? app.getPath("exe") : process.execPath;
return {
supported: true,
reason: null,
executablePath,
appBundlePath:
process.platform === "darwin"
? path.resolve(executablePath, "../../..")
: path.dirname(executablePath),
};
},
"readOpencodeConfig": async (event, ...args) => {
return readOpencodeConfig(String(args[0] ?? "").trim(), String(args[1] ?? "").trim());
},
"writeOpencodeConfig": async (event, ...args) => {
return writeOpencodeConfig(
String(args[0] ?? "").trim(),
String(args[1] ?? "").trim(),
String(args[2] ?? ""),
);
},
"resetOpenworkState": async (event, ...args) => {
return workspaceStore.resetOpenworkState();
},
"resetOpencodeCache": async (event, ...args) => {
return { removed: [], missing: [], errors: [] };
},
"opencodeMcpAuth": async (event, ...args) => {
return runtimeManager.opencodeMcpAuth(String(args[0] ?? "").trim(), String(args[1] ?? "").trim());
},
"setWindowDecorations": async (event, ...args) => {
return undefined;
},
"__openPath": async (event, ...args) => {
const target = String(args[0] ?? "").trim();
if (!target) return "Path is required.";
return shell.openPath(target);
},
"__revealItemInDir": async (event, ...args) => {
const target = String(args[0] ?? "").trim();
if (!target) return "Path is required.";
if (existsSync(target)) {
shell.showItemInFolder(target);
return undefined;
}
// The exact file may not exist yet (or path is slightly off); fall back to
// opening the containing directory so the user still lands in the right place.
const parent = path.dirname(target);
if (parent && parent !== target && existsSync(parent)) {
const error = await shell.openPath(parent);
return error && error.trim() ? error : undefined;
}
return `Could not find "${target}" on disk.`;
},
"__getFileIcon": async (event, ...args) => {
const target = String(args[0] ?? "").trim();
if (!target) return null;
const requestedSize = args[1];
/** @type {"small" | "normal" | "large"} */
let validSize = "normal";
if (requestedSize === "small" || requestedSize === "normal" || requestedSize === "large") {
validSize = requestedSize;
}
try {
const image = await app.getFileIcon(target, { size: validSize });
return image.isEmpty() ? null : image.toDataURL();
} catch {
return null;
}
},
"__applyBrandAppName": async (event, ...args) => {
currentDisplayAppName = applyBrandAppName(
BLANK_SLATE_LAUNCH.enabled || DESKTOP_DISTRIBUTION.flavor === "enterprise" ? null : args[0],
{
fallbackName: APP_NAME,
platform: process.platform,
updateElectronAppName: process.platform === "darwin",
runtimeProcess: process,
app,
applicationMenu,
window: mainWindow,
},
);
if (process.platform === "win32" && !BLANK_SLATE_LAUNCH.enabled) {
await registerWindowsDisplayShortcut();
}
return { ok: true, appName: currentDisplayAppName };
},
"__applyBrandIcon": async (event, ...args) => {
if (BLANK_SLATE_LAUNCH.enabled) return { ok: true };
const value = args[0] === null ? null : String(args[0] ?? "");
return applyBrandIconUrl(value);
},
"__getBrandIconState": async (event, ...args) => {
return getBrandIconState();
},
"__getApplicationsForFile": async (event, ...args) => {
const target = String(args[0] ?? "").trim();
if (!target) return [];
const platform = process.platform;
const results = [];
try {
if (platform === "darwin") {
// Scan /Applications and /System/Applications for .app bundles
const appDirs = ["/Applications", "/System/Applications", "/Applications/Utilities", `${os.homedir()}/Applications`];
const seen = new Set();
for (const dir of appDirs) {
let entries;
try { entries = await readdir(dir); } catch { continue; }
for (const entry of entries) {
if (!entry.endsWith(".app")) continue;
const appPath = path.join(dir, entry);
if (seen.has(appPath)) continue;
seen.add(appPath);
const name = entry.replace(/\.app$/i, "");
let icon = null;
try {
const img = await app.getFileIcon(appPath, { size: "small" });
icon = img.isEmpty() ? null : img.toDataURL();
} catch {}
results.push({ name, appPath, icon });
}
}
} else if (platform === "linux") {
// Parse .desktop files in standard directories
const desktopDirs = ["/usr/share/applications", "/usr/local/share/applications", `${os.homedir()}/.local/share/applications`];
const seen = new Set();
for (const dir of desktopDirs) {
let entries;
try { entries = await readdir(dir); } catch { continue; }
for (const entry of entries) {
if (!entry.endsWith(".desktop")) continue;
const filePath = path.join(dir, entry);
if (seen.has(filePath)) continue;
seen.add(filePath);
try {
const content = await readFile(filePath, "utf-8");
const nameMatch = content.match(/^Name=(.+)$/m);
const execMatch = content.match(/^Exec=(.+)$/m);
if (!nameMatch || !execMatch) continue;
const name = nameMatch[1].trim();
const appPath = execMatch[1].trim().replace(/%[fFuU]/g, "").trim();
if (!appPath) continue;
let icon = null;
try {
const img = await app.getFileIcon(filePath, { size: "small" });
icon = img.isEmpty() ? null : img.toDataURL();
} catch {}
results.push({ name, appPath, icon });
} catch {}
}
}
}
} catch {}
return results;
},
"__openWithApp": async (event, ...args) => {
const target = String(args[0] ?? "").trim();
const appPath = String(args[1] ?? "").trim();
if (!target || !appPath) return "Target and app path are required.";
const platform = process.platform;
try {
if (platform === "darwin") {
execFileSync("open", ["-a", appPath, target]);
} else if (platform === "linux") {
const child = spawn(appPath, [target], { detached: true, stdio: "ignore" });
child.unref();
} else {
return `Open with app is not supported on ${platform}`;
}
} catch (err) {
return String(err?.message ?? err);
}
},
"__fetch": async (event, ...args) => {
const url = String(args[0] ?? "").trim();
const init = args[1] ?? {};
if (!url) throw new Error("URL is required.");
/** @type {RequestInit} */
const requestInit = {
method: typeof init.method === "string" ? init.method : undefined,
headers: init.headers && typeof init.headers === "object" ? init.headers : undefined,
body: typeof init.body === "string" ? init.body : undefined,
credentials: "omit",
cache: "no-store",
};
if (init.agentContextDiagnostics && typeof init.agentContextDiagnostics === "object") {
return fetchAgentContextDiagnosticsResponse(
electronNet.fetch,
url,
requestInit,
init.agentContextDiagnostics.deadlineAtMs,
);
}
const timeoutMs = Number(init.timeoutMs);
const response = await electronNet.fetch(url, {
...requestInit,
signal: Number.isFinite(timeoutMs) && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined,
});
return {
status: response.status,
statusText: response.statusText,
headers: Array.from(response.headers.entries()),
body: await response.text(),
};
},
"__uploadMultipart": async (event, ...args) => {
return runDesktopTransfer(event, args[0] ?? {}, uploadMultipartFromBytes);
},
"__downloadBinary": async (event, ...args) => {
return runDesktopTransfer(event, args[0] ?? {}, downloadBinaryToPath);
},
"__cancelTransfer": async (event, ...args) => {
const controller = activeDesktopTransfers.get(desktopTransferKey(event, args[0]));
if (!controller) return false;
controller.abort();
return true;
},
"__homeDir": async (event, ...args) => {
return os.homedir();
},
"__joinPath": async (event, ...args) => {
return path.join(...args.map((value) => String(value ?? "")));
},
"__setZoomFactor": async (event, ...args) => {
const factor = Number(args[0]);
const window = activeWindowFromEvent(event);
if (!window || !Number.isFinite(factor) || factor <= 0) {
return false;
}
window.webContents.setZoomFactor(factor);
return true;
},
"__setNativeTheme": async (event, ...args) => {
return applyNativeTheme(String(args[0]));
},
"__setApplicationMenuVisible": async (event, ...args) => {
return applicationMenu.setVisible(args[0]);
},
};
if (isDevMode) {
desktopCommandHandlers.__evalRelaunch = async () => {
// Chromium persists localStorage/leveldb lazily; force a flush so the
// relaunched instance sees the same renderer storage (otherwise the app
// can come back signed out and eval flows misread that as a regression).
try {
mainWindow?.webContents.session.flushStorageData();
session.defaultSession.flushStorageData();
} catch {
// Best effort — never block the relaunch on a flush failure.
}
setTimeout(() => {
app.relaunch();
// Graceful quit (not app.exit) so before-quit teardown runs and managed
// sidecars are stopped — a hard exit orphans them and they can hold
// ports (e.g. the CDP debug port) the relaunched instance needs.
app.quit();
}, 150);
return { ok: true };
};
}
function desktopErrorMessageSegment(error, includeName = false) {
try {
if (error && (typeof error === "object" || typeof error === "function")) {
const message = typeof error.message === "string" ? error.message.trim() : "";
if (message) {
const name = typeof error.name === "string" ? error.name.trim() : "";
return includeName && name && name !== "Error" && !message.startsWith(`${name}:`)
? `${name}: ${message}`
: message;
}
}
return String(error);
} catch {
return "Unknown error";
}
}
function desktopErrorCause(error) {
try {
return error && (typeof error === "object" || typeof error === "function") ? error.cause : undefined;
} catch {
return undefined;
}
}
function desktopErrorMessageWithCauses(error) {
try {
const messages = [];
const seenMessages = new Set();
const seenErrors = new Set();
let current = error;
for (let depth = 0; current != null && depth < 8; depth += 1) {
if (typeof current === "object" || typeof current === "function") {
if (seenErrors.has(current)) break;
seenErrors.add(current);
}
const message = desktopErrorMessageSegment(current, depth === 0).trim();
if (message && !seenMessages.has(message)) {
seenMessages.add(message);
messages.push(message);
}
current = desktopErrorCause(current);
}
const combined = messages.join(": ") || "Unknown desktop command error";
return combined.length > 2000 ? `${combined.slice(0, 1997)}...` : combined;
} catch {
return "Unknown desktop command error";
}
}
function assertDesktopActivation() {
if (desktopActivationRequired(
DESKTOP_DISTRIBUTION,
workspaceStore.readDesktopBootstrapConfigSync(),
)) {
throw new Error("OpenWork must be activated from your Den portal before this command is available.");
}
}
async function handleDesktopInvoke(event, command, ...args) {
if (!enterprisePreactivationCommandAllowed(command)) {
assertDesktopActivation();
}
const handler = desktopCommandHandlers[command];
if (!handler) {
throw new Error(`Electron desktop bridge method is not implemented yet: ${command}`);
}
try {
return await handler(event, ...args);
} catch (error) {
throw new Error(desktopErrorMessageWithCauses(error), { cause: error });
}
}
async function createMainWindow() {
if (mainWindow) return mainWindow;
const preloadPath = path.join(__dirname, "preload.mjs");
const windowAppearanceOptions = {};
if (process.platform === "darwin") {
Object.assign(windowAppearanceOptions, {
backgroundColor: "#00000001",
titleBarStyle: "hiddenInset",
vibrancy: macosVibrancyForCurrentTheme(),
visualEffectState: "active",
});
}
const bootSidecar = await readBrandIconSidecar();
const bootSourceUrl = typeof bootSidecar?.sourceUrl === "string" ? bootSidecar.sourceUrl : null;
const cachedBrandImage = bootSourceUrl ? resolveBrandIconImage() : null;
const windowIconImage = cachedBrandImage ?? APP_ICON_IMAGE;
if (process.platform === "win32" && cachedBrandImage && bootSourceUrl) {
try {
const taskbarIconPath = await ensureWindowsBrandIcon(cachedBrandImage);
const taskbarAppId = windowsBrandAppUserModelId(APP_IDENTIFIER, bootSourceUrl);
await registerWindowsBrandShortcut(taskbarAppId, taskbarIconPath);
app.setAppUserModelId(taskbarAppId);
} catch (error) {
console.warn("[brand-icon] failed to register cached Windows shortcut before window creation", error);
}
}
if (process.platform === "darwin" && windowIconImage && !windowIconImage.isEmpty() && app.dock) {
app.dock.setIcon(windowIconImage);
}
mainWindow = new BrowserWindow({
width: 1180,
height: 820,
title: currentDisplayAppName,
show: false,
...(process.platform === "win32" ? { skipTaskbar: true } : {}),
...windowAppearanceOptions,
...(windowIconImage && !windowIconImage.isEmpty() ? { icon: windowIconImage } : {}),
webPreferences: {
// The renderer owns session dispatch + event streams; keep it running
// while hidden/minimized so background tasks are not interrupted.
backgroundThrottling: false,
preload: preloadPath,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
// Enable Chromium's built-in PDF viewer so PDFs render inside the
// artifact panel (<embed> pointed at a blob URL).
plugins: true,
},
});
if (cachedBrandImage && bootSourceUrl) {
await applyCachedBrandIcon(cachedBrandImage, bootSourceUrl);
}
applicationMenu.applyVisibility(mainWindow);
mainWindow.on("page-title-updated", (event) => {
event.preventDefault();
mainWindow?.setTitle(currentDisplayAppName);
});
mainWindow.setTitle(currentDisplayAppName);
mainWindow.once("ready-to-show", () => {
mainWindow?.setTitle(currentDisplayAppName);
if (process.platform === "win32") mainWindow?.setSkipTaskbar(false);
mainWindow?.show();
flushPendingDeepLinks();
});
mainWindow.on("closed", () => {
browserPanel.destroy();
mainWindow = null;
});
const recoverRendererCrash = createRendererCrashRecovery({
reload: () => {
if (!mainWindow || mainWindow.isDestroyed()) return;
mainWindow.webContents.reload();
},
onRepeatedCrash: (details) => {
dialog.showErrorBox(
`${APP_NAME} could not recover`,
`The app renderer stopped repeatedly (${details.reason ?? "unknown reason"}). Quit and reopen OpenWork. Your workspace files were not deleted.`,
);
},
});
mainWindow.webContents.on("render-process-gone", (_event, details) => {
recoverRendererCrash(details);
});
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("file://")) {
try {
runDetachedTask("open local file", () => shell.openPath(fileURLToPath(url)));
} catch {
runDetachedTask("open local file externally", () => openExternalUrl(url));
}
return { action: "deny" };
}
const local =
url.startsWith("http://127.0.0.1") ||
url.startsWith("http://localhost");
if (!local) {
runDetachedTask("open external URL", () => openExternalUrl(url));
return { action: "deny" };
}
return { action: "allow" };
});
mainWindow.webContents.on("will-navigate", (event, url) => {
if (browserPanel.isMainWindowAllowedNavigation(url)) return;
event.preventDefault();
browserPanel.routeBlockedMainWindowNavigation(url);
});
// `will-navigate` does NOT fire for CDP `Page.navigate` (it behaves like
// loadURL), so agent automation that picks the wrong CDP target — the app
// window itself is the first page target when no browser tab exists — used
// to replace the entire workspace UI with the website, with no way back
// (#2000). Catch those at `did-start-navigation`, cancel the load, and
// reroute the URL into a built-in browser tab instead.
mainWindow.webContents.on("did-start-navigation", (_event, url, isInPlace, isMainFrame) => {
if (!isMainFrame || isInPlace) return;
if (browserPanel.isMainWindowAllowedNavigation(url)) return;
try {
mainWindow?.webContents.stop();
} catch {
// best effort — routing below still gives the user a way back
}
browserPanel.routeBlockedMainWindowNavigation(url);
});
const startUrl = process.env.OPENWORK_ELECTRON_START_URL?.trim() || process.env.ELECTRON_START_URL?.trim();
if (startUrl) {
await mainWindow.loadURL(startUrl);
} else {
const packagedIndexPath = path.join(process.resourcesPath, "app-dist", "index.html");
const devIndexPath = path.resolve(__dirname, "../../app/dist/index.html");
await mainWindow.loadFile(app.isPackaged ? packagedIndexPath : devIndexPath);
}
return mainWindow;
}
ipcMain.on("openwork:desktop-bootstrap-sync", (event) => {
event.returnValue = workspaceStore.readDesktopBootstrapConfigSync();
});
ipcMain.on("openwork:desktop-distribution-sync", (event) => {
event.returnValue = DESKTOP_DISTRIBUTION;
});
ipcMain.handle("openwork:desktop", handleDesktopInvoke);
ipcMain.handle("openwork:shell:openExternal", async (_event, url) => {
if (typeof url !== "string" || url.trim().length === 0) {
return { ok: false, error: "empty url" };
}
return openExternalUrl(url.trim());
});
ipcMain.handle("openwork:shell:relaunch", async () => {
app.relaunch();
app.quit();
});
ipcMain.handle("openwork:system:architecture", async () => resolveArchitectureInfo());
ipcMain.handle("openwork:system:microphoneStatus", async () => {
if (process.platform !== "darwin") return { platform: process.platform, status: "not-mac" };
return { platform: process.platform, status: systemPreferences.getMediaAccessStatus("microphone") };
});
ipcMain.handle("openwork:system:askMicrophoneAccess", async () => {
if (process.platform !== "darwin") return { platform: process.platform, granted: true, status: "not-mac" };
const before = systemPreferences.getMediaAccessStatus("microphone");
const granted = await systemPreferences.askForMediaAccess("microphone");
const after = systemPreferences.getMediaAccessStatus("microphone");
return { platform: process.platform, before, after, granted };
});
// ── Terminal IPC ────────────────────────────────────────────────────────
ipcMain.handle("openwork:terminal:create", async (event, options = {}) => {
assertDesktopActivation();
const cwd = await resolveTerminalCwd(options?.cwd);
const cols = Number.isFinite(options?.cols) ? Math.max(20, Math.floor(options.cols)) : 80;
const rows = Number.isFinite(options?.rows) ? Math.max(5, Math.floor(options.rows)) : 24;
const terminalId = `term_${nextTerminalId++}`;
const shellPath = defaultTerminalShell();
const child = pty.spawn(shellPath, [], {
name: "xterm-256color",
cols,
rows,
cwd,
env: {
...process.env,
TERM: "xterm-256color",
COLORTERM: "truecolor",
OPENWORK_TERMINAL: "1",
},
});
terminalProcesses.set(terminalId, { process: child, webContentsId: event.sender.id });
event.sender.once("destroyed", () => killTerminalsForWebContents(event.sender.id));
child.onData((data) => {
if (event.sender.isDestroyed()) return;
event.sender.send("openwork:terminal:data", { terminalId, data });
});
child.onExit(({ exitCode, signal }) => {
terminalProcesses.delete(terminalId);
if (event.sender.isDestroyed()) return;
event.sender.send("openwork:terminal:exit", { terminalId, exitCode, signal });
});
return { terminalId };
});
ipcMain.handle("openwork:terminal:write", (event, terminalId, data) => {
const terminal = terminalForSender(event, terminalId);
if (!terminal || typeof data !== "string") return;
terminal.process.write(data);
});
ipcMain.handle("openwork:terminal:resize", (event, terminalId, cols, rows) => {
const terminal = terminalForSender(event, terminalId);
if (!terminal || !Number.isFinite(cols) || !Number.isFinite(rows)) return;
terminal.process.resize(Math.max(20, Math.floor(cols)), Math.max(5, Math.floor(rows)));
});
ipcMain.handle("openwork:terminal:kill", (event, terminalId) => {
const terminal = terminalForSender(event, terminalId);
if (!terminal) return;
killTerminal(String(terminalId));
});
browserPanel.registerIpc(ipcMain);
registerMigrationIpc({ app, ipcMain });
const { ensureAutoUpdater } = registerUpdaterIpc({
app,
ipcMain,
getMainWindow: () => mainWindow,
// All distributions intentionally share one application identifier, so they also
// share Squirrel's ShipIt domain. Keep the shared default rather than
// implying an isolation the bundle identifier cannot provide.
manifestChannel: DESKTOP_DISTRIBUTION.flavor === "public"
? "latest"
: DESKTOP_DISTRIBUTION.flavor,
electronNet,
shell,
distribution: DESKTOP_DISTRIBUTION.flavor,
platform: process.platform,
arch: process.arch,
});
if (!app.requestSingleInstanceLock()) {
if (isDevMode && !app.isPackaged) {
console.error(`[openwork] Another OpenWork dev instance already holds this profile directory:
${app.getPath("userData")}
The second process is exiting so its CDP port is released.
Run this worktree with an isolated profile: OPENWORK_DEV_PROFILE=auto pnpm dev
or use: pnpm dev:worktree`);
app.exit(1);
setImmediate(() => process.exit(1));
} else {
app.quit();
}
} else {
app.on("before-quit", (event) => {
if (runtimeDisposedForQuit) return;
event.preventDefault();
if (runtimeDisposeInProgress) return;
showShutdownScreen();
desktopAutomationRunner.stop();
runDetachedTask("stop services before quit", async () => {
try {
await Promise.all([
disposeRuntimeBeforeQuit(),
uiControlServer.stop(),
]);
} finally {
scheduleBlankSlateProfileCleanup();
app.quit();
}
});
});
app.on("second-instance", (_event, argv) => {
runDetachedTask("focus second instance", async () => {
const win = await createMainWindow();
if (win.isMinimized()) win.restore();
win.show();
win.focus();
queueDeepLinks(forwardedDeepLinks(argv));
});
});
app.on("open-url", (event, url) => {
event.preventDefault();
runDetachedTask("open deep link", async () => {
const win = await createMainWindow();
if (win.isMinimized()) win.restore();
win.show();
win.focus();
queueDeepLinks([url]);
});
});
app.whenReady().then(async () => {
const systemCaCertificates = await runtimeManager.systemCaCertificates();
session.defaultSession.setCertificateVerifyProc(createSystemCaCertificateVerifyProc(systemCaCertificates));
installMediaPermissionHandlers(session, () => mainWindow);
await runPendingNukeCleanup({
env: process.env,
homedir: os.homedir(),
platform: process.platform,
userDataPath: app.getPath("userData"),
}).catch((error) => {
console.warn("[nuke] pending cleanup failed", error);
});
const bootstrapConfig = await workspaceStore.getDesktopBootstrapConfig();
currentDisplayAppName = applyBrandAppName(
BLANK_SLATE_LAUNCH.enabled || DESKTOP_DISTRIBUTION.flavor === "enterprise"
? null
: bootstrapConfig.brandAppName,
{
fallbackName: APP_NAME,
platform: process.platform,
updateElectronAppName: true,
runtimeProcess: process,
app,
applicationMenu,
},
);
if (process.platform === "win32" && !BLANK_SLATE_LAUNCH.enabled) {
await registerWindowsDisplayShortcut();
}
if (process.platform !== "linux" && !BLANK_SLATE_LAUNCH.enabled) {
await applyDesktopBootstrapBrandIcon(bootstrapConfig, applyBrandIconUrl);
}
applicationMenu.install();
if (!desktopActivationRequired(DESKTOP_DISTRIBUTION, bootstrapConfig)) {
await runtimeManager.prepareFreshRuntime().catch(() => undefined);
}
// Use Tauri's existing workspace state file as canonical so rollback and
// Electron see the same workspace list. Import the short-lived
// Electron-only filename only when the shared file is missing.
await workspaceStore.migrateLegacyElectronWorkspaceStateIfNeeded();
// The UI-control bridge evaluates arbitrary JavaScript in the renderer, so
// it stays down until the installation is activated. Otherwise it is a
// local bypass of the pre-activation restriction.
if (!desktopActivationRequired(DESKTOP_DISTRIBUTION, bootstrapConfig)) {
await uiControlServer.start().catch((error) => {
console.warn("[ui-control] failed to start", error);
});
}
if (!desktopActivationRequired(DESKTOP_DISTRIBUTION, bootstrapConfig)) {
runtimeBootstrapPromise = bootRuntimeForSelectedWorkspace().catch((error) => ({
ok: false,
error: error instanceof Error ? error.message : String(error),
}));
}
queueDeepLinks(forwardedDeepLinks(process.argv));
const win = await createMainWindow();
if (process.platform === "linux" && !BLANK_SLATE_LAUNCH.enabled) {
await applyDesktopBootstrapBrandIcon(bootstrapConfig, applyBrandIconUrl);
}
win.webContents.on("did-finish-load", () => {
flushPendingDeepLinks();
});
if (!BLANK_SLATE_LAUNCH.enabled) {
setTimeout(() => {
void linuxDesktopIntegration.maybePrompt(win).catch((error) => {
console.warn("[desktop-integration] prompt failed", error);
});
}, 500);
}
// Initialize the packaged updater after the window is up so the user sees
// a working app first. Renderer-owned checks pass the selected release
// channel explicitly, avoiding stale stable-feed results for alpha users.
runDetachedTask("initialize updater", ensureAutoUpdater);
}).catch((error) => {
console.error("[desktop] startup failed", error);
dialog.showErrorBox(
`${APP_NAME} could not start`,
"OpenWork hit an unexpected startup error. Quit and reopen the app. If it continues, switch to a Stable build and share the diagnostics with support.",
);
app.quit();
});
app.on("activate", () => {
runDetachedTask("activate window", async () => {
const win = await createMainWindow();
win.show();
win.focus();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
}