@overdeck/desktop 0.45.5 → 0.45.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-electron/main.js +13 -0
- package/dist-electron/main.js.map +1 -1
- package/package.json +1 -1
package/dist-electron/main.js
CHANGED
|
@@ -228,6 +228,7 @@ const SIGTERM_GRACE_MS = 3e3;
|
|
|
228
228
|
let serverProcess = null;
|
|
229
229
|
let restartAttempt = 0;
|
|
230
230
|
let restartTimer = null;
|
|
231
|
+
let waitInterval = null;
|
|
231
232
|
let quitting = false;
|
|
232
233
|
let onReadyCallback = null;
|
|
233
234
|
function randomHex(bytes) {
|
|
@@ -299,20 +300,24 @@ function spawnServer() {
|
|
|
299
300
|
});
|
|
300
301
|
}
|
|
301
302
|
function waitForServer(url, callback, maxMs = 3e4) {
|
|
303
|
+
if (waitInterval) clearInterval(waitInterval);
|
|
302
304
|
const start = Date.now();
|
|
303
305
|
const interval = setInterval(() => {
|
|
304
306
|
if (Date.now() - start > maxMs) {
|
|
305
307
|
clearInterval(interval);
|
|
308
|
+
if (waitInterval === interval) waitInterval = null;
|
|
306
309
|
callback(false);
|
|
307
310
|
return;
|
|
308
311
|
}
|
|
309
312
|
fetch(url + "/api/health", { signal: AbortSignal.timeout(1e3) }).then((r) => {
|
|
310
313
|
if (r.ok) {
|
|
311
314
|
clearInterval(interval);
|
|
315
|
+
if (waitInterval === interval) waitInterval = null;
|
|
312
316
|
callback(true);
|
|
313
317
|
}
|
|
314
318
|
}).catch(() => {});
|
|
315
319
|
}, 500);
|
|
320
|
+
waitInterval = interval;
|
|
316
321
|
}
|
|
317
322
|
function scheduleRestart() {
|
|
318
323
|
if (quitting) return;
|
|
@@ -334,6 +339,10 @@ function stopServer() {
|
|
|
334
339
|
clearTimeout(restartTimer);
|
|
335
340
|
restartTimer = null;
|
|
336
341
|
}
|
|
342
|
+
if (waitInterval) {
|
|
343
|
+
clearInterval(waitInterval);
|
|
344
|
+
waitInterval = null;
|
|
345
|
+
}
|
|
337
346
|
const child = serverProcess;
|
|
338
347
|
if (!child) return;
|
|
339
348
|
serverProcess = null;
|
|
@@ -1112,6 +1121,10 @@ electron.app.on("ready", () => {
|
|
|
1112
1121
|
serverPort = port;
|
|
1113
1122
|
serverUrl = `http://127.0.0.1:${port}`;
|
|
1114
1123
|
serverWsUrl = wsUrl;
|
|
1124
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
1125
|
+
mainWindow.loadURL(resolveWindowUrl());
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1115
1128
|
mainWindow = createWindow();
|
|
1116
1129
|
handleAutoStartNag();
|
|
1117
1130
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","names":["Path","app","FS","nativeImage","Menu","app","Tray","Crypto","FS","ChildProcess","BrowserWindow","autoUpdater","Menu","app","shell","Notification","dialog","Path","FS","Path","app","FS","BrowserWindow","shell","protocol"],"sources":["../src/settings.ts","../src/tray.ts","../src/server.ts","../src/updater.ts","../src/menu.ts","../src/notifications.ts","../src/autostart.ts","../src/protocol.ts","../src/main.ts"],"sourcesContent":["/**\n * Desktop-specific settings, persisted to userData/desktop-settings.json.\n *\n * Covers: tray appearance, per-event notification toggles, auto-start config.\n * Loaded at app startup; updated via IPC from renderer.\n */\n\nimport * as FS from \"node:fs\";\nimport * as Path from \"node:path\";\n\nimport { app } from \"electron\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DesktopSettings {\n tray: {\n showBadge: boolean;\n tooltipDetail: \"minimal\" | \"full\";\n };\n notifications: {\n inputNeeded: boolean;\n stuckAgents: boolean;\n mergeFailures: boolean;\n workComplete: boolean;\n planningDone: boolean;\n mergeReady: boolean;\n };\n autoStart: {\n enabled: boolean;\n nagCount: number;\n nagDismissed: boolean;\n };\n}\n\nexport type NotificationEventType = keyof DesktopSettings[\"notifications\"];\n\n// ─── Defaults ─────────────────────────────────────────────────────────────────\n\nconst DEFAULTS: DesktopSettings = {\n tray: {\n showBadge: true,\n tooltipDetail: \"full\",\n },\n notifications: {\n inputNeeded: true,\n stuckAgents: true,\n mergeFailures: true,\n workComplete: true,\n planningDone: false,\n mergeReady: true,\n },\n autoStart: {\n enabled: false,\n nagCount: 0,\n nagDismissed: false,\n },\n};\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nlet settings: DesktopSettings = deepClone(DEFAULTS);\n\nfunction deepClone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj)) as T;\n}\n\nfunction settingsPath(): string {\n return Path.join(app.getPath(\"userData\"), \"desktop-settings.json\");\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function loadDesktopSettings(): void {\n try {\n const raw = FS.readFileSync(settingsPath(), \"utf8\");\n const parsed = JSON.parse(raw) as Partial<DesktopSettings>;\n settings = {\n tray: { ...DEFAULTS.tray, ...parsed.tray },\n notifications: { ...DEFAULTS.notifications, ...parsed.notifications },\n autoStart: { ...DEFAULTS.autoStart, ...parsed.autoStart },\n };\n } catch {\n settings = deepClone(DEFAULTS);\n }\n}\n\nexport function saveDesktopSettings(): void {\n try {\n FS.writeFileSync(settingsPath(), JSON.stringify(settings, null, 2));\n } catch {\n console.error(\"[desktop] failed to save desktop settings\");\n }\n}\n\nexport function getDesktopSettings(): DesktopSettings {\n return settings;\n}\n\n/**\n * Update a single setting by dotted key (e.g. \"notifications.inputNeeded\").\n * Returns true if the key was found and updated.\n */\nexport function updateDesktopSetting(key: string, value: unknown): boolean {\n const [section, field] = key.split(\".\");\n if (!section || !field) return false;\n const s = settings as unknown as Record<string, Record<string, unknown>>;\n if (!s[section] || !(field in s[section])) return false;\n s[section][field] = value;\n saveDesktopSettings();\n return true;\n}\n","/**\n * System tray for the Overdeck desktop app.\n *\n * Creates a Tray with:\n * - Color-coded icon: green (idle), yellow (working), red (attention needed)\n * - Agent count badge overlay (macOS dock badge)\n * - Rich tooltip: agent count, attention items, last activity\n * - Context menu: Show Dashboard, Start/Stop Cloister, Emergency Stop, Settings, Quit\n *\n * Polls /api/health every 5s for live status.\n */\n\nimport * as Path from \"node:path\";\n\nimport { app, Menu, nativeImage, Tray } from \"electron\";\nimport type { MenuItemConstructorOptions } from \"electron\";\n\nimport { callServerApi, serverUrl, showOrCreateWindow, dispatchMenuAction, isQuitting } from \"./main.js\";\nimport { resolveResourcePath } from \"./main.js\";\nimport { getDesktopSettings } from \"./settings.js\";\n\nexport type AgentStatus = \"idle\" | \"working\" | \"attention\";\n\nlet tray: Tray | null = null;\nlet pollTimer: ReturnType<typeof setInterval> | null = null;\n\n// ─── Icon ─────────────────────────────────────────────────────────────────────\n\nconst STATUS_COLORS: Record<AgentStatus, string> = {\n idle: \"#22c55e\",\n working: \"#f59e0b\",\n attention: \"#ef4444\",\n};\n\nfunction createTrayIcon(status: AgentStatus): Electron.NativeImage {\n const color = STATUS_COLORS[status];\n // 16×16 circle SVG rendered to NativeImage buffer\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\">\n <circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"${color}\" stroke=\"#00000033\" stroke-width=\"0.5\"/>\n <ellipse cx=\"8\" cy=\"8\" rx=\"5.5\" ry=\"3.8\" stroke=\"#ffffff44\" stroke-width=\"0.5\" fill=\"none\"/>\n <circle cx=\"8\" cy=\"8\" r=\"2\" fill=\"#ffffff99\"/>\n <circle cx=\"8\" cy=\"8\" r=\"1\" fill=\"#00000066\"/>\n </svg>`;\n\n return nativeImage.createFromBuffer(Buffer.from(svg));\n}\n\n// ─── Tooltip ──────────────────────────────────────────────────────────────────\n\nfunction buildTooltip(agentCount: number, attentionCount: number, lastActivity: string | null): string {\n const settings = getDesktopSettings();\n if (settings.tray.tooltipDetail === \"minimal\") {\n return `Overdeck — ${agentCount} agent${agentCount !== 1 ? \"s\" : \"\"}`;\n }\n\n const lines = [\"Overdeck\"];\n lines.push(`${agentCount} agent${agentCount !== 1 ? \"s\" : \"\"} running`);\n if (attentionCount > 0) {\n lines.push(`⚠ ${attentionCount} need${attentionCount !== 1 ? \"\" : \"s\"} attention`);\n }\n if (lastActivity) {\n lines.push(`Last: ${lastActivity}`);\n }\n return lines.join(\"\\n\");\n}\n\n// ─── Context menu ─────────────────────────────────────────────────────────────\n\nfunction buildContextMenu(): Electron.Menu {\n const template: MenuItemConstructorOptions[] = [\n {\n label: \"Show Dashboard\",\n click: () => showOrCreateWindow(),\n },\n { type: \"separator\" },\n {\n label: \"Start Cloister\",\n click: () => callServerApi(\"/api/cloister/start\", \"POST\"),\n },\n {\n label: \"Stop Cloister\",\n click: () => callServerApi(\"/api/cloister/stop\", \"POST\"),\n },\n {\n label: \"Emergency Stop All\",\n click: () => callServerApi(\"/api/agents/emergency-stop\", \"POST\"),\n },\n { type: \"separator\" },\n {\n label: \"Settings\",\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n { type: \"separator\" },\n {\n label: \"Quit\",\n click: () => {\n // isQuitting is set by app.on(\"before-quit\")\n app.quit();\n },\n },\n ];\n return Menu.buildFromTemplate(template);\n}\n\n// ─── Polling ──────────────────────────────────────────────────────────────────\n\ninterface HealthResponse {\n agentCount?: number;\n attentionCount?: number;\n lastActivity?: string;\n status?: string;\n}\n\nasync function refreshTrayStatus(): Promise<void> {\n if (!tray || isQuitting) return;\n const url = serverUrl;\n if (!url) return;\n\n try {\n const resp = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(3_000) });\n if (!resp.ok) return;\n\n const data = (await resp.json()) as HealthResponse;\n const agentCount = data.agentCount ?? 0;\n const attentionCount = data.attentionCount ?? 0;\n const lastActivity = data.lastActivity ?? null;\n\n const status: AgentStatus =\n attentionCount > 0 ? \"attention\" : agentCount > 0 ? \"working\" : \"idle\";\n\n tray.setImage(createTrayIcon(status));\n tray.setToolTip(buildTooltip(agentCount, attentionCount, lastActivity));\n tray.setContextMenu(buildContextMenu());\n\n // macOS dock badge\n const settings = getDesktopSettings();\n if (settings.tray.showBadge && process.platform === \"darwin\" && app.dock) {\n app.dock.setBadge(agentCount > 0 ? String(agentCount) : \"\");\n }\n } catch {\n // Server not ready yet — silently skip\n }\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function createTray(): void {\n if (tray) return;\n\n const icon = createTrayIcon(\"idle\");\n tray = new Tray(icon);\n tray.setToolTip(\"Overdeck\");\n tray.setContextMenu(buildContextMenu());\n\n // Single-click opens dashboard on all platforms\n tray.on(\"click\", () => showOrCreateWindow());\n\n // Start polling\n pollTimer = setInterval(() => void refreshTrayStatus(), 5_000);\n void refreshTrayStatus();\n}\n\nexport function destroyTray(): void {\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n tray?.destroy();\n tray = null;\n}\n","/**\n * Embedded dashboard server management.\n *\n * Spawns dist/dashboard/server.js (or the packaged equivalent) as a child\n * process, passing config via environment variables. Supports exponential-\n * backoff restart on crash. Graceful shutdown on app quit.\n *\n * Bootstrap config passed via env vars:\n * OVERDECK_PORT — TCP port for HTTP + WS\n * OVERDECK_AUTH_TOKEN — random hex token (future: auth middleware)\n * OVERDECK_MODE — \"desktop\" (enables desktop-specific behaviours)\n * OVERDECK_NO_BROWSER — \"1\" (suppresses auto browser open)\n */\n\nimport * as ChildProcess from \"node:child_process\";\nimport * as Crypto from \"node:crypto\";\nimport * as FS from \"node:fs\";\n\nimport { resolveServerEntry, resolveServerStaticDir } from \"./main.js\";\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst BASE_PORT = 7825;\nconst MAX_RESTART_DELAY_MS = 30_000;\nconst MAX_RESTART_ATTEMPTS = 8;\nconst SIGTERM_GRACE_MS = 3_000;\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nlet serverProcess: ChildProcess.ChildProcess | null = null;\nlet restartAttempt = 0;\nlet restartTimer: ReturnType<typeof setTimeout> | null = null;\nlet quitting = false;\n\n// Callbacks so main.ts can react to server-ready / URL changes\nlet onReadyCallback: ((port: number, wsUrl: string) => void) | null = null;\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction randomHex(bytes: number): string {\n return Crypto.randomBytes(bytes).toString(\"hex\");\n}\n\nfunction resolvePort(): number {\n // Each restart attempt increments the port to avoid EADDRINUSE during quick restarts\n return BASE_PORT + (restartAttempt % 10);\n}\n\n// ─── Start / Stop ─────────────────────────────────────────────────────────────\n\nexport function startServer(onReady: (port: number, wsUrl: string) => void): void {\n // Reset quit flag so manual restarts (after stopServer) can re-spawn.\n // The app-quit path sets isQuitting in main.ts before stopServer, so this\n // only un-latches the flag when the caller intends a real restart.\n quitting = false;\n // A deliberate (re)start gets a fresh crash budget.\n restartAttempt = 0;\n onReadyCallback = onReady;\n spawnServer();\n}\n\nfunction spawnServer(): void {\n if (quitting) return;\n\n const entry = resolveServerEntry();\n if (!FS.existsSync(entry)) {\n console.error(`[desktop/server] Server entry not found: ${entry}`);\n console.error(\"[desktop/server] Run 'npm run build' to build the dashboard server first.\");\n return;\n }\n\n const port = resolvePort();\n const authToken = randomHex(32);\n\n const child = ChildProcess.spawn(\n process.execPath, // Node.js binary (same version Electron bundles)\n [entry],\n {\n env: {\n ...process.env,\n // The server's config reads API_PORT || PORT (src/dashboard/server/config.ts)\n API_PORT: String(port),\n OVERDECK_PORT: String(port),\n // Packaged layout puts the frontend at resources/server/public, which\n // the server cannot derive from its own bundle path (it assumes a\n // repo-root dist/dashboard/public layout).\n OVERDECK_FRONTEND_DIR: resolveServerStaticDir() ?? undefined,\n OVERDECK_AUTH_TOKEN: authToken,\n OVERDECK_MODE: \"desktop\",\n OVERDECK_NO_BROWSER: \"1\",\n // Terminal settings for Claude Code / tmux rendering\n TERM: process.env.TERM || \"xterm-256color\",\n COLORTERM: process.env.COLORTERM || \"truecolor\",\n LANG: process.env.LANG || \"en_US.UTF-8\",\n // process.execPath is the Electron binary; without this flag a\n // packaged app ignores [entry] and boots the full desktop app again,\n // fork-bombing the machine (PAN-2559).\n ELECTRON_RUN_AS_NODE: \"1\",\n } as NodeJS.ProcessEnv,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n },\n );\n\n serverProcess = child;\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n process.stdout.write(`[server] ${chunk}`);\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n process.stderr.write(`[server] ${chunk}`);\n });\n\n child.on(\"spawn\", () => {\n console.log(`[desktop/server] spawned pid=${child.pid} port=${port}`);\n // Wait for server to be listening before signalling ready\n waitForServer(`http://127.0.0.1:${port}`, (healthy) => {\n console.log(`[desktop/server] ready on port ${port} (healthy=${String(healthy)})`);\n if (healthy) {\n // A confirmed-healthy boot resets the crash budget so the cap only\n // counts consecutive failures.\n restartAttempt = 0;\n }\n onReadyCallback?.(port, `ws://127.0.0.1:${port}`);\n });\n });\n\n child.on(\"exit\", (code, signal) => {\n serverProcess = null;\n console.warn(`[desktop/server] exited code=${String(code)} signal=${String(signal)}`);\n if (!quitting) {\n scheduleRestart();\n }\n });\n\n child.on(\"error\", (err) => {\n console.error(\"[desktop/server] spawn error:\", err);\n serverProcess = null;\n if (!quitting) scheduleRestart();\n });\n}\n\nfunction waitForServer(url: string, callback: (healthy: boolean) => void, maxMs = 30_000): void {\n const start = Date.now();\n const interval = setInterval(() => {\n if (Date.now() - start > maxMs) {\n clearInterval(interval);\n callback(false); // call anyway — server might still come up\n return;\n }\n fetch(url + \"/api/health\", { signal: AbortSignal.timeout(1_000) })\n .then((r) => {\n if (r.ok) {\n clearInterval(interval);\n callback(true);\n }\n })\n .catch(() => {\n /* not ready yet */\n });\n }, 500);\n}\n\nfunction scheduleRestart(): void {\n if (quitting) return;\n restartAttempt++;\n if (restartAttempt > MAX_RESTART_ATTEMPTS) {\n console.error(\n `[desktop/server] server failed ${MAX_RESTART_ATTEMPTS} consecutive times — giving up. ` +\n \"Use the app menu's dashboard restart to retry.\",\n );\n return;\n }\n const delay = Math.min(1_000 * Math.pow(2, restartAttempt - 1), MAX_RESTART_DELAY_MS);\n console.log(`[desktop/server] restarting in ${delay}ms (attempt ${restartAttempt})`);\n restartTimer = setTimeout(() => {\n restartTimer = null;\n spawnServer();\n }, delay);\n}\n\n// ─── Graceful Shutdown ────────────────────────────────────────────────────────\n\nexport function stopServer(): void {\n quitting = true;\n\n if (restartTimer) {\n clearTimeout(restartTimer);\n restartTimer = null;\n }\n\n const child = serverProcess;\n if (!child) return;\n\n serverProcess = null;\n child.kill(\"SIGTERM\");\n\n // Force-kill if it doesn't exit within grace period\n const forceKill = setTimeout(() => {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* already dead */\n }\n }, SIGTERM_GRACE_MS);\n forceKill.unref();\n}\n","/**\n * Auto-updater service using electron-updater.\n *\n * Handles automatic background checks for updates, downloads, and installation.\n * Uses GitHub Releases as the update server.\n */\n\nimport { autoUpdater, UpdateInfo } from \"electron-updater\";\nimport { BrowserWindow } from \"electron\";\n\n// Auto-updater configuration\nconst FOUR_HOURS_MS = 4 * 60 * 60 * 1000;\n\nlet checkIntervalId: ReturnType<typeof setInterval> | null = null;\nlet initialized = false;\n\n// Update state for IPC bridge\nexport interface UpdateStatus {\n checking: boolean;\n available: boolean;\n downloaded: boolean;\n version: string | null;\n error: string | null;\n}\n\nexport let currentStatus: UpdateStatus = {\n checking: false,\n available: false,\n downloaded: false,\n version: null,\n error: null,\n};\n\n// Callbacks for update events\ntype UpdateStatusCallback = (status: UpdateStatus) => void;\nlet statusCallbacks: UpdateStatusCallback[] = [];\n\n/**\n * Register a callback to receive update status changes.\n */\nexport function onUpdateStatusChange(callback: UpdateStatusCallback): void {\n statusCallbacks.push(callback);\n}\n\n/**\n * Notify all listeners of status change\n */\nfunction notifyStatusChange(): void {\n for (const cb of statusCallbacks) {\n cb(currentStatus);\n }\n}\n\n/**\n * Send update event to all browser windows\n */\nfunction broadcastToRenderers(channel: string, ...args: unknown[]): void {\n for (const win of BrowserWindow.getAllWindows()) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...args);\n }\n }\n}\n\n/**\n * Initialize the auto-updater service.\n * Sets up event handlers and starts periodic update checks.\n */\nexport function initializeAutoUpdater(channel: string = \"latest\"): void {\n if (initialized) {\n console.log(\"[updater] Already initialized, skipping...\");\n return;\n }\n initialized = true;\n\n // Configure to use GitHub Releases\n autoUpdater.setFeedURL({\n provider: \"github\",\n owner: \"eltmon\",\n repo: \"overdeck\",\n });\n\n // Respect release channel so canary users don't get stable updates and vice versa\n autoUpdater.channel = channel;\n\n // Don't auto-download - we want to notify user first\n autoUpdater.autoDownload = false;\n autoUpdater.autoInstallOnAppQuit = true;\n\n // Set up event handlers\n autoUpdater.on(\"checking-for-update\", () => {\n currentStatus = { ...currentStatus, checking: true, error: null };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.log(\"[updater] Checking for update...\");\n });\n\n autoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n currentStatus = {\n checking: false,\n available: true,\n downloaded: false,\n version: info.version,\n error: null,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.log(`[updater] Update available: ${info.version}`);\n });\n\n autoUpdater.on(\"update-not-available\", (info: UpdateInfo) => {\n currentStatus = {\n checking: false,\n available: false,\n downloaded: false,\n version: info.version,\n error: null,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.log(`[updater] Update not available. Current version: ${info.version}`);\n });\n\n autoUpdater.on(\"download-progress\", (progressObj) => {\n const percent = progressObj.percent.toFixed(1);\n currentStatus = { ...currentStatus, checking: false };\n notifyStatusChange();\n broadcastToRenderers(\"update-download-progress\", {\n percent,\n transferred: progressObj.transferred,\n total: progressObj.total,\n });\n console.log(`[updater] Download progress: ${percent}%`);\n });\n\n autoUpdater.on(\"update-downloaded\", (info: UpdateInfo) => {\n currentStatus = {\n checking: false,\n available: true,\n downloaded: true,\n version: info.version,\n error: null,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n broadcastToRenderers(\"update-downloaded\", { version: info.version });\n console.log(`[updater] Update downloaded: ${info.version}`);\n });\n\n autoUpdater.on(\"error\", (err: Error) => {\n currentStatus = {\n checking: false,\n available: false,\n downloaded: false,\n version: null,\n error: err.message,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.error(\"[updater] Error:\", err.message);\n });\n\n // Check for updates on app startup (after a short delay to let the app initialize)\n setTimeout(() => {\n checkForUpdates();\n }, 3000);\n\n // Start periodic update checks\n startPeriodicChecks();\n}\n\n/**\n * Start periodic update checks every 4 hours.\n */\nexport function startPeriodicChecks(): void {\n if (checkIntervalId !== null) return; // Already running\n\n checkIntervalId = setInterval(() => {\n checkForUpdates();\n }, FOUR_HOURS_MS);\n\n console.log(\"[updater] Started periodic update checks (every 4 hours)\");\n}\n\n/**\n * Stop periodic update checks.\n */\nexport function stopPeriodicChecks(): void {\n if (checkIntervalId !== null) {\n clearInterval(checkIntervalId);\n checkIntervalId = null;\n console.log(\"[updater] Stopped periodic update checks\");\n }\n}\n\n/**\n * Check for updates manually.\n * Returns a promise that resolves when the check completes.\n */\nexport async function checkForUpdates(): Promise<void> {\n if (currentStatus.checking) {\n console.log(\"[updater] Already checking for update, skipping...\");\n return;\n }\n\n try {\n console.log(\"[updater] Starting update check...\");\n await autoUpdater.checkForUpdates();\n } catch (err) {\n console.error(\"[updater] Check for updates failed:\", err);\n }\n}\n\n/**\n * Download the available update.\n */\nexport async function downloadUpdate(): Promise<void> {\n if (!currentStatus.available) {\n console.log(\"[updater] No update available to download\");\n return;\n }\n\n try {\n console.log(\"[updater] Starting update download...\");\n await autoUpdater.downloadUpdate();\n } catch (err) {\n console.error(\"[updater] Download update failed:\", err);\n }\n}\n\n/**\n * Quit and install the downloaded update.\n */\nexport function quitAndInstall(): void {\n if (!currentStatus.downloaded) {\n console.log(\"[updater] No update downloaded to install\");\n return;\n }\n\n console.log(\"[updater] Quitting and installing update...\");\n autoUpdater.quitAndInstall();\n}\n\n/**\n * Get current update status.\n */\nexport function getUpdateStatus(): UpdateStatus {\n return currentStatus;\n}\n","/**\n * Application menu bar for the Overdeck desktop app.\n *\n * Standard Electron menus: File, Edit, View, Window, Help\n * Plus a Overdeck menu with all orchestration actions.\n *\n * macOS: app name menu with About, Settings, Services etc.\n * Linux/Windows: Settings in File menu.\n */\n\nimport { app, Menu, shell } from \"electron\";\nimport type { MenuItemConstructorOptions } from \"electron\";\n\nimport { callServerApi, showOrCreateWindow, dispatchMenuAction, serverUrl } from \"./main.js\";\nimport { checkForUpdates, quitAndInstall, onUpdateStatusChange } from \"./updater.js\";\n\n// ─── Workspace submenu (refreshed on open) ────────────────────────────────────\n\ninterface WorkspaceSummary {\n issueId: string;\n title?: string;\n}\n\nasync function fetchActiveWorkspaces(): Promise<WorkspaceSummary[]> {\n if (!serverUrl) return [];\n try {\n const resp = await fetch(`${serverUrl}/api/workspaces`, { signal: AbortSignal.timeout(2_000) });\n if (!resp.ok) return [];\n const data = (await resp.json()) as { workspaces?: WorkspaceSummary[] };\n return data.workspaces?.slice(0, 10) ?? [];\n } catch {\n return [];\n }\n}\n\n// ─── Update menu state ─────────────────────────────────────────────────────────\n\nlet updateDownloaded = false;\n\nfunction rebuildMenu(): void {\n const menu = Menu.buildFromTemplate(buildMenuTemplate());\n Menu.setApplicationMenu(menu);\n\n // Re-attach workspace submenu refresh listener\n const overdeckMenu = menu.items.find((item) => item.label === \"Overdeck\");\n if (overdeckMenu?.submenu) {\n overdeckMenu.submenu.on(\"menu-will-show\", () => {\n void fetchActiveWorkspaces().then((workspaces) => {\n const wsItem = overdeckMenu.submenu?.items.find(\n (i) => i.id === \"open-workspace-submenu\",\n );\n if (wsItem) {\n wsItem.label = workspaces.length\n ? `Open Workspace (${workspaces.length})`\n : \"Open Workspace\";\n }\n });\n });\n }\n}\n\nfunction buildWorkspaceSubmenu(workspaces: WorkspaceSummary[]): MenuItemConstructorOptions[] {\n if (workspaces.length === 0) {\n return [{ label: \"No active workspaces\", enabled: false }];\n }\n return workspaces.map((ws) => ({\n label: ws.title ? `${ws.issueId} — ${ws.title}` : ws.issueId,\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(`open-workspace:${ws.issueId}`);\n },\n }));\n}\n\n// ─── Menu template ────────────────────────────────────────────────────────────\n\nfunction buildMenuTemplate(): MenuItemConstructorOptions[] {\n const template: MenuItemConstructorOptions[] = [];\n\n // macOS: app menu (first menu = app name on macOS)\n if (process.platform === \"darwin\") {\n template.push({\n label: app.name,\n submenu: [\n { role: \"about\" },\n { type: \"separator\" },\n {\n label: \"Settings...\",\n accelerator: \"CmdOrCtrl+,\",\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n { type: \"separator\" },\n { role: \"services\" },\n { type: \"separator\" },\n { role: \"hide\" },\n { role: \"hideOthers\" },\n { role: \"unhide\" },\n { type: \"separator\" },\n { role: \"quit\" },\n ],\n });\n }\n\n // File menu\n template.push({\n label: \"File\",\n submenu: [\n ...(process.platform !== \"darwin\"\n ? [\n {\n label: \"Settings...\",\n accelerator: \"CmdOrCtrl+,\",\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n { type: \"separator\" as const },\n ]\n : []),\n { role: process.platform === \"darwin\" ? (\"close\" as const) : (\"quit\" as const) },\n ],\n });\n\n // Edit\n template.push({ role: \"editMenu\" as const });\n\n // View\n template.push({\n label: \"View\",\n submenu: [\n { role: \"reload\" as const },\n { role: \"forceReload\" as const },\n { role: \"toggleDevTools\" as const },\n { type: \"separator\" as const },\n { role: \"resetZoom\" as const },\n { role: \"zoomIn\" as const, accelerator: \"CmdOrCtrl+=\" },\n { role: \"zoomOut\" as const },\n { type: \"separator\" as const },\n { role: \"togglefullscreen\" as const },\n ],\n });\n\n // Window\n template.push({ role: \"windowMenu\" as const });\n\n // Overdeck\n template.push({\n label: \"Overdeck\",\n submenu: [\n {\n label: \"Start Cloister\",\n click: () => callServerApi(\"/api/cloister/start\", \"POST\"),\n },\n {\n label: \"Stop Cloister\",\n click: () => callServerApi(\"/api/cloister/stop\", \"POST\"),\n },\n {\n label: \"Emergency Stop All Agents\",\n click: () => callServerApi(\"/api/agents/emergency-stop\", \"POST\"),\n },\n { type: \"separator\" },\n {\n label: \"Open Workspace\",\n id: \"open-workspace-submenu\",\n submenu: [{ label: \"Loading...\", enabled: false }],\n },\n { type: \"separator\" },\n {\n label: \"Settings...\",\n accelerator: process.platform !== \"darwin\" ? \"CmdOrCtrl+,\" : undefined,\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n ],\n });\n\n // Help\n template.push({\n role: \"help\" as const,\n submenu: [\n {\n label: \"Check for Updates...\",\n click: () => {\n void checkForUpdates();\n },\n },\n { type: \"separator\" },\n {\n label: \"Flywheel Documentation\",\n click: () =>\n void shell.openExternal(\n \"https://github.com/eltmon/overdeck/blob/main/docs/FLYWHEEL.md\",\n ),\n },\n {\n label: \"Overdeck on GitHub\",\n click: () =>\n void shell.openExternal(\"https://github.com/eltmon/overdeck\"),\n },\n {\n label: \"Report an Issue\",\n click: () =>\n void shell.openExternal(\"https://github.com/eltmon/overdeck/issues\"),\n },\n ],\n });\n\n // Add \"Install Update and Restart\" item if update is downloaded\n if (updateDownloaded) {\n const helpMenu = template[template.length - 1];\n if (helpMenu && helpMenu.submenu && Array.isArray(helpMenu.submenu)) {\n helpMenu.submenu.push(\n { type: \"separator\" },\n {\n label: \"Install Update and Restart\",\n click: () => {\n quitAndInstall();\n },\n },\n );\n }\n }\n\n return template;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function configureApplicationMenu(): void {\n const menu = Menu.buildFromTemplate(buildMenuTemplate());\n Menu.setApplicationMenu(menu);\n\n // Refresh workspace submenu when the Overdeck menu is about to open\n const overdeckMenu = menu.items.find((item) => item.label === \"Overdeck\");\n if (overdeckMenu?.submenu) {\n overdeckMenu.submenu.on(\"menu-will-show\", () => {\n void fetchActiveWorkspaces().then((workspaces) => {\n const wsItem = overdeckMenu.submenu?.items.find(\n (i) => i.id === \"open-workspace-submenu\",\n );\n if (wsItem) {\n // Electron doesn't support dynamic submenu replacement after build,\n // so we dispatch a menu action that opens a workspace picker in the renderer\n wsItem.label = workspaces.length\n ? `Open Workspace (${workspaces.length})`\n : \"Open Workspace\";\n }\n });\n });\n }\n\n // Listen for update status changes to rebuild menu when update is downloaded\n onUpdateStatusChange((status) => {\n if (status.downloaded && !updateDownloaded) {\n updateDownloaded = true;\n rebuildMenu();\n }\n });\n}\n","/**\n * Native desktop notifications for Overdeck events.\n *\n * Event types and their default enabled state (all configurable in Settings):\n * inputNeeded — Agent needs user input (default: on)\n * stuckAgents — Agent has been stuck > threshold (default: on)\n * mergeFailures — Merge specialist failed (default: on)\n * workComplete — Agent signalled work done (default: on)\n * planningDone — Planning session complete (default: off)\n * mergeReady — PR ready for human merge (default: on)\n *\n * Notifications are sent from:\n * 1. Renderer → main via IPC (pan:notify) — for events the frontend detects\n * 2. Main process directly — in future when subscribing to domain events via WS\n */\n\nimport { app, ipcMain, Notification } from \"electron\";\n\nimport { IPC } from \"./main.js\";\nimport { showOrCreateWindow } from \"./main.js\";\nimport { getDesktopSettings } from \"./settings.js\";\nimport { resolveResourcePath } from \"./main.js\";\nimport type { NotificationEventType } from \"./settings.js\";\n\n// ─── Send notification ────────────────────────────────────────────────────────\n\nexport function sendNotification(\n eventType: NotificationEventType,\n title: string,\n body: string,\n): void {\n const settings = getDesktopSettings();\n if (!settings.notifications[eventType]) return;\n\n if (!Notification.isSupported()) return;\n\n const iconPath = resolveResourcePath(\"icon.png\");\n\n const notification = new Notification({\n title,\n body,\n icon: iconPath ?? undefined,\n silent: false,\n });\n\n notification.on(\"click\", () => showOrCreateWindow());\n notification.show();\n}\n\n// ─── IPC handler registration ─────────────────────────────────────────────────\n\nexport function registerNotificationHandlers(): void {\n ipcMain.handle(IPC.NOTIFY, (_event, eventType: unknown, title: unknown, body: unknown) => {\n if (\n typeof eventType === \"string\" &&\n typeof title === \"string\" &&\n typeof body === \"string\"\n ) {\n sendNotification(eventType as NotificationEventType, title, body);\n }\n });\n}\n\n// ─── App lifecycle integration ────────────────────────────────────────────────\n\n/**\n * Called at app.ready to request notification permission on macOS.\n * On Linux/Windows, Notification.isSupported() handles availability.\n */\nexport function initializeNotifications(): void {\n if (!Notification.isSupported()) {\n console.log(\"[desktop/notifications] native notifications not supported on this platform\");\n }\n}\n","/**\n * Auto-start nag flow for the Overdeck desktop app.\n *\n * Flow:\n * Launch 1: Full explanation dialog (warm, inviting)\n * Launch 2-5: In-app toasts dispatched to renderer via \"auto-start-nag:<n>:<max>\" menu action\n * - Shows: \"Reminder N of 5\", Enable button (prominent), Not Yet, Stop Reminding Me\n * After 5 or \"Stop reminding me\": never prompt again (nagDismissed = true)\n * Once enabled: auto-start registered via app.setLoginItemSettings\n *\n * State tracked in DesktopSettings.autoStart via settings.ts.\n */\n\nimport { app, dialog } from \"electron\";\n\nimport { getDesktopSettings, updateDesktopSetting } from \"./settings.js\";\nimport { dispatchMenuAction } from \"./main.js\";\n\nconst NAG_MAX = 5;\n\n// ─── Enable auto-start ────────────────────────────────────────────────────────\n\nexport function enableAutoStart(): void {\n updateDesktopSetting(\"autoStart.enabled\", true);\n updateDesktopSetting(\"autoStart.nagDismissed\", true);\n app.setLoginItemSettings({ openAtLogin: true });\n}\n\nexport function dismissAutoStartNag(): void {\n updateDesktopSetting(\"autoStart.nagDismissed\", true);\n}\n\n// ─── Nag flow ─────────────────────────────────────────────────────────────────\n\nexport function handleAutoStartNag(): void {\n const { autoStart } = getDesktopSettings();\n\n if (autoStart.enabled || autoStart.nagDismissed) return;\n if (autoStart.nagCount >= NAG_MAX) {\n updateDesktopSetting(\"autoStart.nagDismissed\", true);\n return;\n }\n\n const count = autoStart.nagCount + 1;\n updateDesktopSetting(\"autoStart.nagCount\", count);\n\n if (count === 1) {\n // First launch: native dialog\n showFirstLaunchDialog();\n } else {\n // Subsequent launches: in-app toast dispatched to renderer\n // Dispatched with delay to let the window finish loading\n setTimeout(() => {\n dispatchMenuAction(`auto-start-nag:${count}:${NAG_MAX}`);\n }, 3_500);\n }\n}\n\nfunction showFirstLaunchDialog(): void {\n // Delay until window is ready\n setTimeout(() => {\n void dialog\n .showMessageBox({\n type: \"info\",\n title: \"Start Overdeck automatically?\",\n message: \"Keep an eye on your agents — even when you forget to open the app.\",\n detail:\n \"Overdeck can start automatically when you log in, so you never miss an agent \" +\n \"asking for help or a merge that's ready to ship.\\n\\n\" +\n \"You can change this at any time in Settings → Desktop → Auto-start.\",\n buttons: [\"Enable Auto-start\", \"Not Yet\"],\n defaultId: 0,\n cancelId: 1,\n })\n .then(({ response }) => {\n if (response === 0) {\n enableAutoStart();\n }\n });\n }, 2_000);\n}\n","/**\n * overdeck:// custom protocol for serving static frontend assets in packaged builds.\n *\n * In dev mode:\n * BrowserWindow loads from Vite dev server URL (VITE_DEV_SERVER_URL env var).\n * HMR and source maps work normally.\n *\n * In packaged builds:\n * BrowserWindow loads overdeck://app/index.html.\n * This protocol handler serves files from the bundled dist/dashboard/public/.\n * WebSocket connections (ws/rpc, ws/terminal) go to the embedded server port\n * on localhost — the protocol handler only serves static assets.\n *\n * Security:\n * - Path traversal protection: rejects paths containing \"..\"\n * - Only files within the static root are served\n * - Non-existent asset requests return -6 (net::ERR_FILE_NOT_FOUND)\n * - HTML routes (no extension) fall back to index.html for SPA routing\n *\n * CSP:\n * Renderer CSP is configured via vite.config.ts in the frontend.\n * In packaged mode, connect-src must include ws://127.0.0.1:* for WebSocket.\n */\n\nimport * as FS from \"node:fs\";\nimport * as Path from \"node:path\";\n\nimport { protocol } from \"electron\";\n\nimport { DESKTOP_SCHEME, resolveServerStaticDir } from \"./main.js\";\n\nlet registered = false;\n\n// ─── Path resolution ──────────────────────────────────────────────────────────\n\nexport function resolveStaticPath(staticRoot: string, requestUrl: string): string {\n const fallbackIndex = Path.join(staticRoot, \"index.html\");\n\n let url: URL;\n try {\n url = new URL(requestUrl);\n } catch {\n return fallbackIndex;\n }\n\n const rawPath = decodeURIComponent(url.pathname);\n const normalized = Path.posix.normalize(rawPath).replace(/^\\/+/, \"\");\n\n if (normalized.includes(\"..\")) {\n return fallbackIndex;\n }\n\n const requestedPath = normalized.length > 0 ? normalized : \"index.html\";\n const resolved = Path.resolve(staticRoot, requestedPath);\n\n const staticRootResolved = Path.resolve(staticRoot);\n const inRoot =\n resolved === staticRootResolved ||\n resolved.startsWith(staticRootResolved + Path.sep);\n\n if (!inRoot) return fallbackIndex;\n\n // If path has a file extension, serve it (or 404)\n if (Path.extname(resolved)) {\n return resolved;\n }\n\n // SPA route: check for index.html in subdirectory, else root index\n const nestedIndex = Path.join(resolved, \"index.html\");\n if (FS.existsSync(nestedIndex)) return nestedIndex;\n\n return fallbackIndex;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Register the overdeck:// protocol handler.\n * Must be called after app.ready (but registration via registerSchemesAsPrivileged\n * must happen before app.ready — done in main.ts).\n */\nexport function registerDesktopProtocol(): void {\n if (registered) return;\n\n const staticRoot = resolveServerStaticDir();\n if (!staticRoot) {\n console.error(\n \"[desktop/protocol] Static bundle not found — packaged frontend assets missing. \" +\n \"Run 'npm run build:dashboard' first.\",\n );\n return;\n }\n\n const fallbackIndex = Path.join(staticRoot, \"index.html\");\n\n protocol.registerFileProtocol(DESKTOP_SCHEME, (request, callback) => {\n const candidate = resolveStaticPath(staticRoot, request.url);\n const hasExt = Path.extname(candidate).length > 0;\n\n if (!FS.existsSync(candidate)) {\n if (hasExt) {\n // Asset not found — return 404\n callback({ error: -6 /* net::ERR_FILE_NOT_FOUND */ });\n } else {\n // SPA route — fall back to index\n callback({ path: fallbackIndex });\n }\n return;\n }\n\n callback({ path: candidate });\n });\n\n registered = true;\n console.log(`[desktop/protocol] registered ${DESKTOP_SCHEME}:// serving from ${staticRoot}`);\n}\n","import * as FS from \"node:fs\";\nimport * as OS from \"node:os\";\nimport * as Path from \"node:path\";\n\nimport {\n app,\n BrowserWindow,\n ipcMain,\n protocol,\n shell,\n} from \"electron\";\n\nimport { createTray, destroyTray } from \"./tray.js\";\nimport { loadDesktopSettings, getDesktopSettings, updateDesktopSetting } from \"./settings.js\";\nimport { startServer, stopServer } from \"./server.js\";\nimport { configureApplicationMenu } from \"./menu.js\";\nimport { initializeNotifications, registerNotificationHandlers } from \"./notifications.js\";\nimport { handleAutoStartNag } from \"./autostart.js\";\nimport { registerDesktopProtocol } from \"./protocol.js\";\nimport { initializeAutoUpdater, checkForUpdates, downloadUpdate, quitAndInstall, getUpdateStatus, onUpdateStatusChange } from \"./updater.js\";\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\nconst ROOT_DIR = Path.resolve(__dirname, \"../../..\");\nconst isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);\nconst APP_DISPLAY_NAME = isDevelopment ? \"Overdeck (Dev)\" : \"Overdeck\";\nconst APP_ID = \"com.overdeck.app\";\nconst LINUX_WM_CLASS = isDevelopment ? \"overdeck-dev\" : \"overdeck\";\nexport const DESKTOP_SCHEME = \"overdeck\";\n\n// IPC channel names\nexport const IPC = {\n GET_SERVER_URL: \"pan:get-server-url\",\n GET_WS_URL: \"pan:get-ws-url\",\n PICK_FOLDER: \"pan:pick-folder\",\n OPEN_EXTERNAL: \"pan:open-external\",\n OPEN_TERMINAL_WINDOW: \"pan:open-terminal-window\",\n SET_ALWAYS_ON_TOP: \"pan:set-always-on-top\",\n MENU_ACTION: \"pan:menu-action\",\n GET_DESKTOP_SETTINGS: \"pan:get-desktop-settings\",\n UPDATE_DESKTOP_SETTING: \"pan:update-desktop-setting\",\n NOTIFY: \"pan:notify\",\n GET_UPDATE_STATUS: \"pan:get-update-status\",\n CHECK_FOR_UPDATES: \"pan:check-for-updates\",\n DOWNLOAD_UPDATE: \"pan:download-update\",\n QUIT_AND_INSTALL: \"pan:quit-and-install\",\n RESTART_DASHBOARD: \"pan:restart-dashboard\",\n} as const;\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nlet mainWindow: BrowserWindow | null = null;\nexport let serverPort = 0;\nexport let serverUrl = \"\";\nexport let serverWsUrl = \"\";\nexport let isQuitting = false;\nconst terminalWindows = new Map<string, BrowserWindow>();\n\n// ─── Single instance lock ─────────────────────────────────────────────────────\n// A second launch must not boot another full app (window, tray, embedded\n// server). Duplicates exit immediately and the running instance is focused\n// via the second-instance event below (PAN-2559).\n\nif (!app.requestSingleInstanceLock()) {\n app.exit(0);\n}\n\napp.on(\"second-instance\", () => showOrCreateWindow());\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nexport function resolveResourcePath(fileName: string): string | null {\n const candidates = [\n // Packaged (electron-builder): resources are in process.resourcesPath/resources/\n Path.join(process.resourcesPath ?? \"\", \"resources\", fileName),\n // npx / global-install: resources are bundled at <package>/resources/\n Path.join(__dirname, \"../resources\", fileName),\n // Dev: resources are in the monorepo\n Path.join(ROOT_DIR, \"apps/desktop/resources\", fileName),\n ];\n for (const candidate of candidates) {\n if (FS.existsSync(candidate)) return candidate;\n }\n return null;\n}\n\nexport function resolveServerEntry(): string {\n if (app.isPackaged) {\n return Path.join(process.resourcesPath ?? \"\", \"server/server.js\");\n }\n // npx / global-install: server bundle is at <package>/server/server.js\n const npxBundle = Path.join(__dirname, \"../server/server.js\");\n if (FS.existsSync(npxBundle)) return npxBundle;\n // Dev mode: server is built in the monorepo root\n return Path.join(ROOT_DIR, \"dist/dashboard/server.js\");\n}\n\nexport function resolveServerStaticDir(): string | null {\n const candidates: string[] = [];\n if (app.isPackaged) {\n candidates.push(Path.join(process.resourcesPath ?? \"\", \"server/public\"));\n } else {\n // npx / global-install: static assets are at <package>/server/public\n candidates.push(Path.join(__dirname, \"../server/public\"));\n // Dev mode fallback\n candidates.push(Path.join(ROOT_DIR, \"dist/dashboard/public\"));\n }\n for (const candidate of candidates) {\n if (FS.existsSync(Path.join(candidate, \"index.html\"))) {\n return candidate;\n }\n }\n return null;\n}\n\nexport function resolveWindowUrl(): string {\n if (isDevelopment) {\n return process.env.VITE_DEV_SERVER_URL!;\n }\n // Serve the window from the embedded server's HTTP origin so the frontend's\n // relative /api fetches and /ws/* sockets are same-origin, exactly like the\n // browser dashboard. The overdeck:// protocol page has no working data\n // layer (relative fetches resolve to the app bundle, PAN-2561) — it is only\n // the pre-server fallback.\n if (serverUrl) {\n return serverUrl;\n }\n return `${DESKTOP_SCHEME}://app/index.html`;\n}\n\nfunction createTerminalWindow(sessionName: string, title: string): BrowserWindow {\n const win = new BrowserWindow({\n width: 900,\n height: 650,\n title,\n webPreferences: {\n preload: Path.join(__dirname, \"preload.js\"),\n contextIsolation: true,\n nodeIntegration: false,\n sandbox: false,\n },\n show: false,\n });\n\n win.once(\"ready-to-show\", () => win.show());\n void win.loadURL(`${resolveWindowUrl()}?terminal=${encodeURIComponent(sessionName)}&title=${encodeURIComponent(title)}`);\n return win;\n}\n\n// ─── IPC: basic handlers ──────────────────────────────────────────────────────\n\nfunction registerIpcHandlers(): void {\n ipcMain.on(IPC.GET_SERVER_URL, (event) => {\n event.returnValue = serverUrl;\n });\n\n ipcMain.on(IPC.GET_WS_URL, (event) => {\n event.returnValue = serverWsUrl;\n });\n\n ipcMain.handle(IPC.PICK_FOLDER, async () => {\n const { dialog } = await import(\"electron\");\n const result = await dialog.showOpenDialog({ properties: [\"openDirectory\"] });\n return result.canceled ? null : (result.filePaths[0] ?? null);\n });\n\n ipcMain.handle(IPC.OPEN_EXTERNAL, async (_event, url: unknown) => {\n if (typeof url !== \"string\") return;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return;\n await shell.openExternal(parsed.toString());\n });\n\n ipcMain.handle(IPC.GET_DESKTOP_SETTINGS, () => getDesktopSettings());\n\n ipcMain.on(IPC.OPEN_TERMINAL_WINDOW, (_event, sessionName: unknown, title: unknown) => {\n if (typeof sessionName !== \"string\" || typeof title !== \"string\") return;\n\n const existing = terminalWindows.get(sessionName);\n if (existing && !existing.isDestroyed()) {\n if (!existing.isVisible()) existing.show();\n existing.focus();\n return;\n }\n\n const win = createTerminalWindow(sessionName, title);\n terminalWindows.set(sessionName, win);\n win.on(\"closed\", () => terminalWindows.delete(sessionName));\n });\n\n ipcMain.on(IPC.SET_ALWAYS_ON_TOP, (_event, value: unknown) => {\n const focused = BrowserWindow.getFocusedWindow();\n if (focused) {\n focused.setAlwaysOnTop(value === true);\n }\n });\n\n ipcMain.handle(IPC.UPDATE_DESKTOP_SETTING, (_event, key: unknown, value: unknown) => {\n if (typeof key !== \"string\") return;\n const updated = updateDesktopSetting(key, value);\n // Apply auto-start setting immediately\n if (updated && key === \"autoStart.enabled\") {\n app.setLoginItemSettings({ openAtLogin: value === true });\n }\n });\n\n // Update IPC handlers\n ipcMain.handle(IPC.GET_UPDATE_STATUS, () => getUpdateStatus());\n\n ipcMain.handle(IPC.CHECK_FOR_UPDATES, async () => {\n try {\n await checkForUpdates();\n } catch (err) {\n console.error(\"[main] checkForUpdates failed:\", err);\n }\n return getUpdateStatus();\n });\n\n ipcMain.handle(IPC.DOWNLOAD_UPDATE, async () => {\n try {\n await downloadUpdate();\n } catch (err) {\n console.error(\"[main] downloadUpdate failed:\", err);\n }\n return getUpdateStatus();\n });\n\n ipcMain.on(IPC.QUIT_AND_INSTALL, () => {\n quitAndInstall();\n });\n\n ipcMain.handle(IPC.RESTART_DASHBOARD, async () => {\n console.log(\"[main] manual dashboard restart requested\");\n stopServer();\n // Brief delay so SIGTERM has a chance to land before we re-spawn.\n await new Promise((resolve) => setTimeout(resolve, 500));\n startServer((port, wsUrl) => {\n serverPort = port;\n serverUrl = `http://127.0.0.1:${port}`;\n serverWsUrl = wsUrl;\n console.log(`[main] dashboard restarted on ${serverUrl}`);\n });\n });\n}\n\n// ─── Window ───────────────────────────────────────────────────────────────────\n\nexport function createWindow(): BrowserWindow {\n const win = new BrowserWindow({\n width: 1400,\n height: 900,\n minWidth: 900,\n minHeight: 600,\n title: APP_DISPLAY_NAME,\n webPreferences: {\n preload: Path.join(__dirname, \"preload.js\"),\n contextIsolation: true,\n nodeIntegration: false,\n sandbox: false,\n },\n show: false,\n });\n\n win.once(\"ready-to-show\", () => win.show());\n\n win.on(\"close\", (event) => {\n if (!isQuitting && process.platform === \"darwin\") {\n event.preventDefault();\n win.hide();\n }\n });\n\n void win.loadURL(resolveWindowUrl());\n return win;\n}\n\nexport function showOrCreateWindow(): void {\n const existing =\n BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0];\n if (existing) {\n if (!existing.isVisible()) existing.show();\n existing.focus();\n return;\n }\n mainWindow = createWindow();\n}\n\nexport function dispatchMenuAction(action: string): void {\n const win = BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0];\n if (!win) {\n showOrCreateWindow();\n setTimeout(() => dispatchMenuAction(action), 500);\n return;\n }\n const send = () => {\n if (win.isDestroyed()) return;\n win.webContents.send(IPC.MENU_ACTION, action);\n if (!win.isVisible()) win.show();\n win.focus();\n };\n if (win.webContents.isLoadingMainFrame()) {\n win.webContents.once(\"did-finish-load\", send);\n } else {\n send();\n }\n}\n\n// ─── Server API helper ────────────────────────────────────────────────────────\n\nexport function callServerApi(path: string, method: string): void {\n if (!serverUrl) return;\n fetch(`${serverUrl}${path}`, { method }).catch((err: unknown) => {\n console.error(\"[desktop] server API call failed:\", err);\n });\n}\n\n// ─── Protocol registration (before app.ready) ─────────────────────────────────\n\nprotocol.registerSchemesAsPrivileged([\n {\n scheme: DESKTOP_SCHEME,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n },\n },\n]);\n\nif (process.platform === \"linux\") {\n app.commandLine.appendSwitch(\"class\", LINUX_WM_CLASS);\n}\n\n// ─── App Lifecycle ────────────────────────────────────────────────────────────\n\napp.on(\"ready\", () => {\n loadDesktopSettings();\n registerIpcHandlers();\n registerNotificationHandlers();\n initializeNotifications();\n configureApplicationMenu();\n registerDesktopProtocol();\n const updateChannel = app.getVersion().includes(\"-canary\") ? \"beta\" : \"latest\";\n initializeAutoUpdater(updateChannel);\n\n if (process.platform === \"win32\") {\n app.setAppUserModelId(APP_ID);\n }\n\n if (process.platform === \"darwin\" && app.dock) {\n const iconPath = resolveResourcePath(\"icon.png\");\n if (iconPath) app.dock.setIcon(iconPath);\n }\n\n createTray();\n\n startServer((port, wsUrl) => {\n serverPort = port;\n serverUrl = `http://127.0.0.1:${port}`;\n serverWsUrl = wsUrl;\n mainWindow = createWindow();\n handleAutoStartNag();\n });\n});\n\napp.on(\"window-all-closed\", () => {\n if (process.platform !== \"darwin\") {\n isQuitting = true;\n app.quit();\n }\n});\n\napp.on(\"activate\", () => showOrCreateWindow());\n\napp.on(\"before-quit\", () => {\n isQuitting = true;\n destroyTray();\n stopServer();\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"main.js","names":["Path","app","FS","nativeImage","Menu","app","Tray","Crypto","FS","ChildProcess","BrowserWindow","autoUpdater","Menu","app","shell","Notification","dialog","Path","FS","Path","app","FS","BrowserWindow","shell","protocol"],"sources":["../src/settings.ts","../src/tray.ts","../src/server.ts","../src/updater.ts","../src/menu.ts","../src/notifications.ts","../src/autostart.ts","../src/protocol.ts","../src/main.ts"],"sourcesContent":["/**\n * Desktop-specific settings, persisted to userData/desktop-settings.json.\n *\n * Covers: tray appearance, per-event notification toggles, auto-start config.\n * Loaded at app startup; updated via IPC from renderer.\n */\n\nimport * as FS from \"node:fs\";\nimport * as Path from \"node:path\";\n\nimport { app } from \"electron\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DesktopSettings {\n tray: {\n showBadge: boolean;\n tooltipDetail: \"minimal\" | \"full\";\n };\n notifications: {\n inputNeeded: boolean;\n stuckAgents: boolean;\n mergeFailures: boolean;\n workComplete: boolean;\n planningDone: boolean;\n mergeReady: boolean;\n };\n autoStart: {\n enabled: boolean;\n nagCount: number;\n nagDismissed: boolean;\n };\n}\n\nexport type NotificationEventType = keyof DesktopSettings[\"notifications\"];\n\n// ─── Defaults ─────────────────────────────────────────────────────────────────\n\nconst DEFAULTS: DesktopSettings = {\n tray: {\n showBadge: true,\n tooltipDetail: \"full\",\n },\n notifications: {\n inputNeeded: true,\n stuckAgents: true,\n mergeFailures: true,\n workComplete: true,\n planningDone: false,\n mergeReady: true,\n },\n autoStart: {\n enabled: false,\n nagCount: 0,\n nagDismissed: false,\n },\n};\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nlet settings: DesktopSettings = deepClone(DEFAULTS);\n\nfunction deepClone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj)) as T;\n}\n\nfunction settingsPath(): string {\n return Path.join(app.getPath(\"userData\"), \"desktop-settings.json\");\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function loadDesktopSettings(): void {\n try {\n const raw = FS.readFileSync(settingsPath(), \"utf8\");\n const parsed = JSON.parse(raw) as Partial<DesktopSettings>;\n settings = {\n tray: { ...DEFAULTS.tray, ...parsed.tray },\n notifications: { ...DEFAULTS.notifications, ...parsed.notifications },\n autoStart: { ...DEFAULTS.autoStart, ...parsed.autoStart },\n };\n } catch {\n settings = deepClone(DEFAULTS);\n }\n}\n\nexport function saveDesktopSettings(): void {\n try {\n FS.writeFileSync(settingsPath(), JSON.stringify(settings, null, 2));\n } catch {\n console.error(\"[desktop] failed to save desktop settings\");\n }\n}\n\nexport function getDesktopSettings(): DesktopSettings {\n return settings;\n}\n\n/**\n * Update a single setting by dotted key (e.g. \"notifications.inputNeeded\").\n * Returns true if the key was found and updated.\n */\nexport function updateDesktopSetting(key: string, value: unknown): boolean {\n const [section, field] = key.split(\".\");\n if (!section || !field) return false;\n const s = settings as unknown as Record<string, Record<string, unknown>>;\n if (!s[section] || !(field in s[section])) return false;\n s[section][field] = value;\n saveDesktopSettings();\n return true;\n}\n","/**\n * System tray for the Overdeck desktop app.\n *\n * Creates a Tray with:\n * - Color-coded icon: green (idle), yellow (working), red (attention needed)\n * - Agent count badge overlay (macOS dock badge)\n * - Rich tooltip: agent count, attention items, last activity\n * - Context menu: Show Dashboard, Start/Stop Cloister, Emergency Stop, Settings, Quit\n *\n * Polls /api/health every 5s for live status.\n */\n\nimport * as Path from \"node:path\";\n\nimport { app, Menu, nativeImage, Tray } from \"electron\";\nimport type { MenuItemConstructorOptions } from \"electron\";\n\nimport { callServerApi, serverUrl, showOrCreateWindow, dispatchMenuAction, isQuitting } from \"./main.js\";\nimport { resolveResourcePath } from \"./main.js\";\nimport { getDesktopSettings } from \"./settings.js\";\n\nexport type AgentStatus = \"idle\" | \"working\" | \"attention\";\n\nlet tray: Tray | null = null;\nlet pollTimer: ReturnType<typeof setInterval> | null = null;\n\n// ─── Icon ─────────────────────────────────────────────────────────────────────\n\nconst STATUS_COLORS: Record<AgentStatus, string> = {\n idle: \"#22c55e\",\n working: \"#f59e0b\",\n attention: \"#ef4444\",\n};\n\nfunction createTrayIcon(status: AgentStatus): Electron.NativeImage {\n const color = STATUS_COLORS[status];\n // 16×16 circle SVG rendered to NativeImage buffer\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\">\n <circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"${color}\" stroke=\"#00000033\" stroke-width=\"0.5\"/>\n <ellipse cx=\"8\" cy=\"8\" rx=\"5.5\" ry=\"3.8\" stroke=\"#ffffff44\" stroke-width=\"0.5\" fill=\"none\"/>\n <circle cx=\"8\" cy=\"8\" r=\"2\" fill=\"#ffffff99\"/>\n <circle cx=\"8\" cy=\"8\" r=\"1\" fill=\"#00000066\"/>\n </svg>`;\n\n return nativeImage.createFromBuffer(Buffer.from(svg));\n}\n\n// ─── Tooltip ──────────────────────────────────────────────────────────────────\n\nfunction buildTooltip(agentCount: number, attentionCount: number, lastActivity: string | null): string {\n const settings = getDesktopSettings();\n if (settings.tray.tooltipDetail === \"minimal\") {\n return `Overdeck — ${agentCount} agent${agentCount !== 1 ? \"s\" : \"\"}`;\n }\n\n const lines = [\"Overdeck\"];\n lines.push(`${agentCount} agent${agentCount !== 1 ? \"s\" : \"\"} running`);\n if (attentionCount > 0) {\n lines.push(`⚠ ${attentionCount} need${attentionCount !== 1 ? \"\" : \"s\"} attention`);\n }\n if (lastActivity) {\n lines.push(`Last: ${lastActivity}`);\n }\n return lines.join(\"\\n\");\n}\n\n// ─── Context menu ─────────────────────────────────────────────────────────────\n\nfunction buildContextMenu(): Electron.Menu {\n const template: MenuItemConstructorOptions[] = [\n {\n label: \"Show Dashboard\",\n click: () => showOrCreateWindow(),\n },\n { type: \"separator\" },\n {\n label: \"Start Cloister\",\n click: () => callServerApi(\"/api/cloister/start\", \"POST\"),\n },\n {\n label: \"Stop Cloister\",\n click: () => callServerApi(\"/api/cloister/stop\", \"POST\"),\n },\n {\n label: \"Emergency Stop All\",\n click: () => callServerApi(\"/api/agents/emergency-stop\", \"POST\"),\n },\n { type: \"separator\" },\n {\n label: \"Settings\",\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n { type: \"separator\" },\n {\n label: \"Quit\",\n click: () => {\n // isQuitting is set by app.on(\"before-quit\")\n app.quit();\n },\n },\n ];\n return Menu.buildFromTemplate(template);\n}\n\n// ─── Polling ──────────────────────────────────────────────────────────────────\n\ninterface HealthResponse {\n agentCount?: number;\n attentionCount?: number;\n lastActivity?: string;\n status?: string;\n}\n\nasync function refreshTrayStatus(): Promise<void> {\n if (!tray || isQuitting) return;\n const url = serverUrl;\n if (!url) return;\n\n try {\n const resp = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(3_000) });\n if (!resp.ok) return;\n\n const data = (await resp.json()) as HealthResponse;\n const agentCount = data.agentCount ?? 0;\n const attentionCount = data.attentionCount ?? 0;\n const lastActivity = data.lastActivity ?? null;\n\n const status: AgentStatus =\n attentionCount > 0 ? \"attention\" : agentCount > 0 ? \"working\" : \"idle\";\n\n tray.setImage(createTrayIcon(status));\n tray.setToolTip(buildTooltip(agentCount, attentionCount, lastActivity));\n tray.setContextMenu(buildContextMenu());\n\n // macOS dock badge\n const settings = getDesktopSettings();\n if (settings.tray.showBadge && process.platform === \"darwin\" && app.dock) {\n app.dock.setBadge(agentCount > 0 ? String(agentCount) : \"\");\n }\n } catch {\n // Server not ready yet — silently skip\n }\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function createTray(): void {\n if (tray) return;\n\n const icon = createTrayIcon(\"idle\");\n tray = new Tray(icon);\n tray.setToolTip(\"Overdeck\");\n tray.setContextMenu(buildContextMenu());\n\n // Single-click opens dashboard on all platforms\n tray.on(\"click\", () => showOrCreateWindow());\n\n // Start polling\n pollTimer = setInterval(() => void refreshTrayStatus(), 5_000);\n void refreshTrayStatus();\n}\n\nexport function destroyTray(): void {\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n tray?.destroy();\n tray = null;\n}\n","/**\n * Embedded dashboard server management.\n *\n * Spawns dist/dashboard/server.js (or the packaged equivalent) as a child\n * process, passing config via environment variables. Supports exponential-\n * backoff restart on crash. Graceful shutdown on app quit.\n *\n * Bootstrap config passed via env vars:\n * OVERDECK_PORT — TCP port for HTTP + WS\n * OVERDECK_AUTH_TOKEN — random hex token (future: auth middleware)\n * OVERDECK_MODE — \"desktop\" (enables desktop-specific behaviours)\n * OVERDECK_NO_BROWSER — \"1\" (suppresses auto browser open)\n */\n\nimport * as ChildProcess from \"node:child_process\";\nimport * as Crypto from \"node:crypto\";\nimport * as FS from \"node:fs\";\n\nimport { resolveServerEntry, resolveServerStaticDir } from \"./main.js\";\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst BASE_PORT = 7825;\nconst MAX_RESTART_DELAY_MS = 30_000;\nconst MAX_RESTART_ATTEMPTS = 8;\nconst SIGTERM_GRACE_MS = 3_000;\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nlet serverProcess: ChildProcess.ChildProcess | null = null;\nlet restartAttempt = 0;\nlet restartTimer: ReturnType<typeof setTimeout> | null = null;\nlet waitInterval: ReturnType<typeof setInterval> | null = null;\nlet quitting = false;\n\n// Callbacks so main.ts can react to server-ready / URL changes\nlet onReadyCallback: ((port: number, wsUrl: string) => void) | null = null;\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction randomHex(bytes: number): string {\n return Crypto.randomBytes(bytes).toString(\"hex\");\n}\n\nfunction resolvePort(): number {\n // Each restart attempt increments the port to avoid EADDRINUSE during quick restarts\n return BASE_PORT + (restartAttempt % 10);\n}\n\n// ─── Start / Stop ─────────────────────────────────────────────────────────────\n\nexport function startServer(onReady: (port: number, wsUrl: string) => void): void {\n // Reset quit flag so manual restarts (after stopServer) can re-spawn.\n // The app-quit path sets isQuitting in main.ts before stopServer, so this\n // only un-latches the flag when the caller intends a real restart.\n quitting = false;\n // A deliberate (re)start gets a fresh crash budget.\n restartAttempt = 0;\n onReadyCallback = onReady;\n spawnServer();\n}\n\nfunction spawnServer(): void {\n if (quitting) return;\n\n const entry = resolveServerEntry();\n if (!FS.existsSync(entry)) {\n console.error(`[desktop/server] Server entry not found: ${entry}`);\n console.error(\"[desktop/server] Run 'npm run build' to build the dashboard server first.\");\n return;\n }\n\n const port = resolvePort();\n const authToken = randomHex(32);\n\n const child = ChildProcess.spawn(\n process.execPath, // Node.js binary (same version Electron bundles)\n [entry],\n {\n env: {\n ...process.env,\n // The server's config reads API_PORT || PORT (src/dashboard/server/config.ts)\n API_PORT: String(port),\n OVERDECK_PORT: String(port),\n // Packaged layout puts the frontend at resources/server/public, which\n // the server cannot derive from its own bundle path (it assumes a\n // repo-root dist/dashboard/public layout).\n OVERDECK_FRONTEND_DIR: resolveServerStaticDir() ?? undefined,\n OVERDECK_AUTH_TOKEN: authToken,\n OVERDECK_MODE: \"desktop\",\n OVERDECK_NO_BROWSER: \"1\",\n // Terminal settings for Claude Code / tmux rendering\n TERM: process.env.TERM || \"xterm-256color\",\n COLORTERM: process.env.COLORTERM || \"truecolor\",\n LANG: process.env.LANG || \"en_US.UTF-8\",\n // process.execPath is the Electron binary; without this flag a\n // packaged app ignores [entry] and boots the full desktop app again,\n // fork-bombing the machine (PAN-2559).\n ELECTRON_RUN_AS_NODE: \"1\",\n } as NodeJS.ProcessEnv,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n },\n );\n\n serverProcess = child;\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n process.stdout.write(`[server] ${chunk}`);\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n process.stderr.write(`[server] ${chunk}`);\n });\n\n child.on(\"spawn\", () => {\n console.log(`[desktop/server] spawned pid=${child.pid} port=${port}`);\n // Wait for server to be listening before signalling ready\n waitForServer(`http://127.0.0.1:${port}`, (healthy) => {\n console.log(`[desktop/server] ready on port ${port} (healthy=${String(healthy)})`);\n if (healthy) {\n // A confirmed-healthy boot resets the crash budget so the cap only\n // counts consecutive failures.\n restartAttempt = 0;\n }\n onReadyCallback?.(port, `ws://127.0.0.1:${port}`);\n });\n });\n\n child.on(\"exit\", (code, signal) => {\n serverProcess = null;\n console.warn(`[desktop/server] exited code=${String(code)} signal=${String(signal)}`);\n if (!quitting) {\n scheduleRestart();\n }\n });\n\n child.on(\"error\", (err) => {\n console.error(\"[desktop/server] spawn error:\", err);\n serverProcess = null;\n if (!quitting) scheduleRestart();\n });\n}\n\nfunction waitForServer(url: string, callback: (healthy: boolean) => void, maxMs = 30_000): void {\n // Only one poller may be live: a crash-restart spawns a new child on a new\n // port, and a stale poller's late callback would clobber serverUrl with the\n // dead port (and, pre-PAN-2570, open another window).\n if (waitInterval) clearInterval(waitInterval);\n const start = Date.now();\n const interval = setInterval(() => {\n if (Date.now() - start > maxMs) {\n clearInterval(interval);\n if (waitInterval === interval) waitInterval = null;\n callback(false); // call anyway — server might still come up\n return;\n }\n fetch(url + \"/api/health\", { signal: AbortSignal.timeout(1_000) })\n .then((r) => {\n if (r.ok) {\n clearInterval(interval);\n if (waitInterval === interval) waitInterval = null;\n callback(true);\n }\n })\n .catch(() => {\n /* not ready yet */\n });\n }, 500);\n waitInterval = interval;\n}\n\nfunction scheduleRestart(): void {\n if (quitting) return;\n restartAttempt++;\n if (restartAttempt > MAX_RESTART_ATTEMPTS) {\n console.error(\n `[desktop/server] server failed ${MAX_RESTART_ATTEMPTS} consecutive times — giving up. ` +\n \"Use the app menu's dashboard restart to retry.\",\n );\n return;\n }\n const delay = Math.min(1_000 * Math.pow(2, restartAttempt - 1), MAX_RESTART_DELAY_MS);\n console.log(`[desktop/server] restarting in ${delay}ms (attempt ${restartAttempt})`);\n restartTimer = setTimeout(() => {\n restartTimer = null;\n spawnServer();\n }, delay);\n}\n\n// ─── Graceful Shutdown ────────────────────────────────────────────────────────\n\nexport function stopServer(): void {\n quitting = true;\n\n if (restartTimer) {\n clearTimeout(restartTimer);\n restartTimer = null;\n }\n\n if (waitInterval) {\n clearInterval(waitInterval);\n waitInterval = null;\n }\n\n const child = serverProcess;\n if (!child) return;\n\n serverProcess = null;\n child.kill(\"SIGTERM\");\n\n // Force-kill if it doesn't exit within grace period\n const forceKill = setTimeout(() => {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* already dead */\n }\n }, SIGTERM_GRACE_MS);\n forceKill.unref();\n}\n","/**\n * Auto-updater service using electron-updater.\n *\n * Handles automatic background checks for updates, downloads, and installation.\n * Uses GitHub Releases as the update server.\n */\n\nimport { autoUpdater, UpdateInfo } from \"electron-updater\";\nimport { BrowserWindow } from \"electron\";\n\n// Auto-updater configuration\nconst FOUR_HOURS_MS = 4 * 60 * 60 * 1000;\n\nlet checkIntervalId: ReturnType<typeof setInterval> | null = null;\nlet initialized = false;\n\n// Update state for IPC bridge\nexport interface UpdateStatus {\n checking: boolean;\n available: boolean;\n downloaded: boolean;\n version: string | null;\n error: string | null;\n}\n\nexport let currentStatus: UpdateStatus = {\n checking: false,\n available: false,\n downloaded: false,\n version: null,\n error: null,\n};\n\n// Callbacks for update events\ntype UpdateStatusCallback = (status: UpdateStatus) => void;\nlet statusCallbacks: UpdateStatusCallback[] = [];\n\n/**\n * Register a callback to receive update status changes.\n */\nexport function onUpdateStatusChange(callback: UpdateStatusCallback): void {\n statusCallbacks.push(callback);\n}\n\n/**\n * Notify all listeners of status change\n */\nfunction notifyStatusChange(): void {\n for (const cb of statusCallbacks) {\n cb(currentStatus);\n }\n}\n\n/**\n * Send update event to all browser windows\n */\nfunction broadcastToRenderers(channel: string, ...args: unknown[]): void {\n for (const win of BrowserWindow.getAllWindows()) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...args);\n }\n }\n}\n\n/**\n * Initialize the auto-updater service.\n * Sets up event handlers and starts periodic update checks.\n */\nexport function initializeAutoUpdater(channel: string = \"latest\"): void {\n if (initialized) {\n console.log(\"[updater] Already initialized, skipping...\");\n return;\n }\n initialized = true;\n\n // Configure to use GitHub Releases\n autoUpdater.setFeedURL({\n provider: \"github\",\n owner: \"eltmon\",\n repo: \"overdeck\",\n });\n\n // Respect release channel so canary users don't get stable updates and vice versa\n autoUpdater.channel = channel;\n\n // Don't auto-download - we want to notify user first\n autoUpdater.autoDownload = false;\n autoUpdater.autoInstallOnAppQuit = true;\n\n // Set up event handlers\n autoUpdater.on(\"checking-for-update\", () => {\n currentStatus = { ...currentStatus, checking: true, error: null };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.log(\"[updater] Checking for update...\");\n });\n\n autoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n currentStatus = {\n checking: false,\n available: true,\n downloaded: false,\n version: info.version,\n error: null,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.log(`[updater] Update available: ${info.version}`);\n });\n\n autoUpdater.on(\"update-not-available\", (info: UpdateInfo) => {\n currentStatus = {\n checking: false,\n available: false,\n downloaded: false,\n version: info.version,\n error: null,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.log(`[updater] Update not available. Current version: ${info.version}`);\n });\n\n autoUpdater.on(\"download-progress\", (progressObj) => {\n const percent = progressObj.percent.toFixed(1);\n currentStatus = { ...currentStatus, checking: false };\n notifyStatusChange();\n broadcastToRenderers(\"update-download-progress\", {\n percent,\n transferred: progressObj.transferred,\n total: progressObj.total,\n });\n console.log(`[updater] Download progress: ${percent}%`);\n });\n\n autoUpdater.on(\"update-downloaded\", (info: UpdateInfo) => {\n currentStatus = {\n checking: false,\n available: true,\n downloaded: true,\n version: info.version,\n error: null,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n broadcastToRenderers(\"update-downloaded\", { version: info.version });\n console.log(`[updater] Update downloaded: ${info.version}`);\n });\n\n autoUpdater.on(\"error\", (err: Error) => {\n currentStatus = {\n checking: false,\n available: false,\n downloaded: false,\n version: null,\n error: err.message,\n };\n notifyStatusChange();\n broadcastToRenderers(\"update-status\", currentStatus);\n console.error(\"[updater] Error:\", err.message);\n });\n\n // Check for updates on app startup (after a short delay to let the app initialize)\n setTimeout(() => {\n checkForUpdates();\n }, 3000);\n\n // Start periodic update checks\n startPeriodicChecks();\n}\n\n/**\n * Start periodic update checks every 4 hours.\n */\nexport function startPeriodicChecks(): void {\n if (checkIntervalId !== null) return; // Already running\n\n checkIntervalId = setInterval(() => {\n checkForUpdates();\n }, FOUR_HOURS_MS);\n\n console.log(\"[updater] Started periodic update checks (every 4 hours)\");\n}\n\n/**\n * Stop periodic update checks.\n */\nexport function stopPeriodicChecks(): void {\n if (checkIntervalId !== null) {\n clearInterval(checkIntervalId);\n checkIntervalId = null;\n console.log(\"[updater] Stopped periodic update checks\");\n }\n}\n\n/**\n * Check for updates manually.\n * Returns a promise that resolves when the check completes.\n */\nexport async function checkForUpdates(): Promise<void> {\n if (currentStatus.checking) {\n console.log(\"[updater] Already checking for update, skipping...\");\n return;\n }\n\n try {\n console.log(\"[updater] Starting update check...\");\n await autoUpdater.checkForUpdates();\n } catch (err) {\n console.error(\"[updater] Check for updates failed:\", err);\n }\n}\n\n/**\n * Download the available update.\n */\nexport async function downloadUpdate(): Promise<void> {\n if (!currentStatus.available) {\n console.log(\"[updater] No update available to download\");\n return;\n }\n\n try {\n console.log(\"[updater] Starting update download...\");\n await autoUpdater.downloadUpdate();\n } catch (err) {\n console.error(\"[updater] Download update failed:\", err);\n }\n}\n\n/**\n * Quit and install the downloaded update.\n */\nexport function quitAndInstall(): void {\n if (!currentStatus.downloaded) {\n console.log(\"[updater] No update downloaded to install\");\n return;\n }\n\n console.log(\"[updater] Quitting and installing update...\");\n autoUpdater.quitAndInstall();\n}\n\n/**\n * Get current update status.\n */\nexport function getUpdateStatus(): UpdateStatus {\n return currentStatus;\n}\n","/**\n * Application menu bar for the Overdeck desktop app.\n *\n * Standard Electron menus: File, Edit, View, Window, Help\n * Plus a Overdeck menu with all orchestration actions.\n *\n * macOS: app name menu with About, Settings, Services etc.\n * Linux/Windows: Settings in File menu.\n */\n\nimport { app, Menu, shell } from \"electron\";\nimport type { MenuItemConstructorOptions } from \"electron\";\n\nimport { callServerApi, showOrCreateWindow, dispatchMenuAction, serverUrl } from \"./main.js\";\nimport { checkForUpdates, quitAndInstall, onUpdateStatusChange } from \"./updater.js\";\n\n// ─── Workspace submenu (refreshed on open) ────────────────────────────────────\n\ninterface WorkspaceSummary {\n issueId: string;\n title?: string;\n}\n\nasync function fetchActiveWorkspaces(): Promise<WorkspaceSummary[]> {\n if (!serverUrl) return [];\n try {\n const resp = await fetch(`${serverUrl}/api/workspaces`, { signal: AbortSignal.timeout(2_000) });\n if (!resp.ok) return [];\n const data = (await resp.json()) as { workspaces?: WorkspaceSummary[] };\n return data.workspaces?.slice(0, 10) ?? [];\n } catch {\n return [];\n }\n}\n\n// ─── Update menu state ─────────────────────────────────────────────────────────\n\nlet updateDownloaded = false;\n\nfunction rebuildMenu(): void {\n const menu = Menu.buildFromTemplate(buildMenuTemplate());\n Menu.setApplicationMenu(menu);\n\n // Re-attach workspace submenu refresh listener\n const overdeckMenu = menu.items.find((item) => item.label === \"Overdeck\");\n if (overdeckMenu?.submenu) {\n overdeckMenu.submenu.on(\"menu-will-show\", () => {\n void fetchActiveWorkspaces().then((workspaces) => {\n const wsItem = overdeckMenu.submenu?.items.find(\n (i) => i.id === \"open-workspace-submenu\",\n );\n if (wsItem) {\n wsItem.label = workspaces.length\n ? `Open Workspace (${workspaces.length})`\n : \"Open Workspace\";\n }\n });\n });\n }\n}\n\nfunction buildWorkspaceSubmenu(workspaces: WorkspaceSummary[]): MenuItemConstructorOptions[] {\n if (workspaces.length === 0) {\n return [{ label: \"No active workspaces\", enabled: false }];\n }\n return workspaces.map((ws) => ({\n label: ws.title ? `${ws.issueId} — ${ws.title}` : ws.issueId,\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(`open-workspace:${ws.issueId}`);\n },\n }));\n}\n\n// ─── Menu template ────────────────────────────────────────────────────────────\n\nfunction buildMenuTemplate(): MenuItemConstructorOptions[] {\n const template: MenuItemConstructorOptions[] = [];\n\n // macOS: app menu (first menu = app name on macOS)\n if (process.platform === \"darwin\") {\n template.push({\n label: app.name,\n submenu: [\n { role: \"about\" },\n { type: \"separator\" },\n {\n label: \"Settings...\",\n accelerator: \"CmdOrCtrl+,\",\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n { type: \"separator\" },\n { role: \"services\" },\n { type: \"separator\" },\n { role: \"hide\" },\n { role: \"hideOthers\" },\n { role: \"unhide\" },\n { type: \"separator\" },\n { role: \"quit\" },\n ],\n });\n }\n\n // File menu\n template.push({\n label: \"File\",\n submenu: [\n ...(process.platform !== \"darwin\"\n ? [\n {\n label: \"Settings...\",\n accelerator: \"CmdOrCtrl+,\",\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n { type: \"separator\" as const },\n ]\n : []),\n { role: process.platform === \"darwin\" ? (\"close\" as const) : (\"quit\" as const) },\n ],\n });\n\n // Edit\n template.push({ role: \"editMenu\" as const });\n\n // View\n template.push({\n label: \"View\",\n submenu: [\n { role: \"reload\" as const },\n { role: \"forceReload\" as const },\n { role: \"toggleDevTools\" as const },\n { type: \"separator\" as const },\n { role: \"resetZoom\" as const },\n { role: \"zoomIn\" as const, accelerator: \"CmdOrCtrl+=\" },\n { role: \"zoomOut\" as const },\n { type: \"separator\" as const },\n { role: \"togglefullscreen\" as const },\n ],\n });\n\n // Window\n template.push({ role: \"windowMenu\" as const });\n\n // Overdeck\n template.push({\n label: \"Overdeck\",\n submenu: [\n {\n label: \"Start Cloister\",\n click: () => callServerApi(\"/api/cloister/start\", \"POST\"),\n },\n {\n label: \"Stop Cloister\",\n click: () => callServerApi(\"/api/cloister/stop\", \"POST\"),\n },\n {\n label: \"Emergency Stop All Agents\",\n click: () => callServerApi(\"/api/agents/emergency-stop\", \"POST\"),\n },\n { type: \"separator\" },\n {\n label: \"Open Workspace\",\n id: \"open-workspace-submenu\",\n submenu: [{ label: \"Loading...\", enabled: false }],\n },\n { type: \"separator\" },\n {\n label: \"Settings...\",\n accelerator: process.platform !== \"darwin\" ? \"CmdOrCtrl+,\" : undefined,\n click: () => {\n showOrCreateWindow();\n dispatchMenuAction(\"open-settings\");\n },\n },\n ],\n });\n\n // Help\n template.push({\n role: \"help\" as const,\n submenu: [\n {\n label: \"Check for Updates...\",\n click: () => {\n void checkForUpdates();\n },\n },\n { type: \"separator\" },\n {\n label: \"Flywheel Documentation\",\n click: () =>\n void shell.openExternal(\n \"https://github.com/eltmon/overdeck/blob/main/docs/FLYWHEEL.md\",\n ),\n },\n {\n label: \"Overdeck on GitHub\",\n click: () =>\n void shell.openExternal(\"https://github.com/eltmon/overdeck\"),\n },\n {\n label: \"Report an Issue\",\n click: () =>\n void shell.openExternal(\"https://github.com/eltmon/overdeck/issues\"),\n },\n ],\n });\n\n // Add \"Install Update and Restart\" item if update is downloaded\n if (updateDownloaded) {\n const helpMenu = template[template.length - 1];\n if (helpMenu && helpMenu.submenu && Array.isArray(helpMenu.submenu)) {\n helpMenu.submenu.push(\n { type: \"separator\" },\n {\n label: \"Install Update and Restart\",\n click: () => {\n quitAndInstall();\n },\n },\n );\n }\n }\n\n return template;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function configureApplicationMenu(): void {\n const menu = Menu.buildFromTemplate(buildMenuTemplate());\n Menu.setApplicationMenu(menu);\n\n // Refresh workspace submenu when the Overdeck menu is about to open\n const overdeckMenu = menu.items.find((item) => item.label === \"Overdeck\");\n if (overdeckMenu?.submenu) {\n overdeckMenu.submenu.on(\"menu-will-show\", () => {\n void fetchActiveWorkspaces().then((workspaces) => {\n const wsItem = overdeckMenu.submenu?.items.find(\n (i) => i.id === \"open-workspace-submenu\",\n );\n if (wsItem) {\n // Electron doesn't support dynamic submenu replacement after build,\n // so we dispatch a menu action that opens a workspace picker in the renderer\n wsItem.label = workspaces.length\n ? `Open Workspace (${workspaces.length})`\n : \"Open Workspace\";\n }\n });\n });\n }\n\n // Listen for update status changes to rebuild menu when update is downloaded\n onUpdateStatusChange((status) => {\n if (status.downloaded && !updateDownloaded) {\n updateDownloaded = true;\n rebuildMenu();\n }\n });\n}\n","/**\n * Native desktop notifications for Overdeck events.\n *\n * Event types and their default enabled state (all configurable in Settings):\n * inputNeeded — Agent needs user input (default: on)\n * stuckAgents — Agent has been stuck > threshold (default: on)\n * mergeFailures — Merge specialist failed (default: on)\n * workComplete — Agent signalled work done (default: on)\n * planningDone — Planning session complete (default: off)\n * mergeReady — PR ready for human merge (default: on)\n *\n * Notifications are sent from:\n * 1. Renderer → main via IPC (pan:notify) — for events the frontend detects\n * 2. Main process directly — in future when subscribing to domain events via WS\n */\n\nimport { app, ipcMain, Notification } from \"electron\";\n\nimport { IPC } from \"./main.js\";\nimport { showOrCreateWindow } from \"./main.js\";\nimport { getDesktopSettings } from \"./settings.js\";\nimport { resolveResourcePath } from \"./main.js\";\nimport type { NotificationEventType } from \"./settings.js\";\n\n// ─── Send notification ────────────────────────────────────────────────────────\n\nexport function sendNotification(\n eventType: NotificationEventType,\n title: string,\n body: string,\n): void {\n const settings = getDesktopSettings();\n if (!settings.notifications[eventType]) return;\n\n if (!Notification.isSupported()) return;\n\n const iconPath = resolveResourcePath(\"icon.png\");\n\n const notification = new Notification({\n title,\n body,\n icon: iconPath ?? undefined,\n silent: false,\n });\n\n notification.on(\"click\", () => showOrCreateWindow());\n notification.show();\n}\n\n// ─── IPC handler registration ─────────────────────────────────────────────────\n\nexport function registerNotificationHandlers(): void {\n ipcMain.handle(IPC.NOTIFY, (_event, eventType: unknown, title: unknown, body: unknown) => {\n if (\n typeof eventType === \"string\" &&\n typeof title === \"string\" &&\n typeof body === \"string\"\n ) {\n sendNotification(eventType as NotificationEventType, title, body);\n }\n });\n}\n\n// ─── App lifecycle integration ────────────────────────────────────────────────\n\n/**\n * Called at app.ready to request notification permission on macOS.\n * On Linux/Windows, Notification.isSupported() handles availability.\n */\nexport function initializeNotifications(): void {\n if (!Notification.isSupported()) {\n console.log(\"[desktop/notifications] native notifications not supported on this platform\");\n }\n}\n","/**\n * Auto-start nag flow for the Overdeck desktop app.\n *\n * Flow:\n * Launch 1: Full explanation dialog (warm, inviting)\n * Launch 2-5: In-app toasts dispatched to renderer via \"auto-start-nag:<n>:<max>\" menu action\n * - Shows: \"Reminder N of 5\", Enable button (prominent), Not Yet, Stop Reminding Me\n * After 5 or \"Stop reminding me\": never prompt again (nagDismissed = true)\n * Once enabled: auto-start registered via app.setLoginItemSettings\n *\n * State tracked in DesktopSettings.autoStart via settings.ts.\n */\n\nimport { app, dialog } from \"electron\";\n\nimport { getDesktopSettings, updateDesktopSetting } from \"./settings.js\";\nimport { dispatchMenuAction } from \"./main.js\";\n\nconst NAG_MAX = 5;\n\n// ─── Enable auto-start ────────────────────────────────────────────────────────\n\nexport function enableAutoStart(): void {\n updateDesktopSetting(\"autoStart.enabled\", true);\n updateDesktopSetting(\"autoStart.nagDismissed\", true);\n app.setLoginItemSettings({ openAtLogin: true });\n}\n\nexport function dismissAutoStartNag(): void {\n updateDesktopSetting(\"autoStart.nagDismissed\", true);\n}\n\n// ─── Nag flow ─────────────────────────────────────────────────────────────────\n\nexport function handleAutoStartNag(): void {\n const { autoStart } = getDesktopSettings();\n\n if (autoStart.enabled || autoStart.nagDismissed) return;\n if (autoStart.nagCount >= NAG_MAX) {\n updateDesktopSetting(\"autoStart.nagDismissed\", true);\n return;\n }\n\n const count = autoStart.nagCount + 1;\n updateDesktopSetting(\"autoStart.nagCount\", count);\n\n if (count === 1) {\n // First launch: native dialog\n showFirstLaunchDialog();\n } else {\n // Subsequent launches: in-app toast dispatched to renderer\n // Dispatched with delay to let the window finish loading\n setTimeout(() => {\n dispatchMenuAction(`auto-start-nag:${count}:${NAG_MAX}`);\n }, 3_500);\n }\n}\n\nfunction showFirstLaunchDialog(): void {\n // Delay until window is ready\n setTimeout(() => {\n void dialog\n .showMessageBox({\n type: \"info\",\n title: \"Start Overdeck automatically?\",\n message: \"Keep an eye on your agents — even when you forget to open the app.\",\n detail:\n \"Overdeck can start automatically when you log in, so you never miss an agent \" +\n \"asking for help or a merge that's ready to ship.\\n\\n\" +\n \"You can change this at any time in Settings → Desktop → Auto-start.\",\n buttons: [\"Enable Auto-start\", \"Not Yet\"],\n defaultId: 0,\n cancelId: 1,\n })\n .then(({ response }) => {\n if (response === 0) {\n enableAutoStart();\n }\n });\n }, 2_000);\n}\n","/**\n * overdeck:// custom protocol for serving static frontend assets in packaged builds.\n *\n * In dev mode:\n * BrowserWindow loads from Vite dev server URL (VITE_DEV_SERVER_URL env var).\n * HMR and source maps work normally.\n *\n * In packaged builds:\n * BrowserWindow loads overdeck://app/index.html.\n * This protocol handler serves files from the bundled dist/dashboard/public/.\n * WebSocket connections (ws/rpc, ws/terminal) go to the embedded server port\n * on localhost — the protocol handler only serves static assets.\n *\n * Security:\n * - Path traversal protection: rejects paths containing \"..\"\n * - Only files within the static root are served\n * - Non-existent asset requests return -6 (net::ERR_FILE_NOT_FOUND)\n * - HTML routes (no extension) fall back to index.html for SPA routing\n *\n * CSP:\n * Renderer CSP is configured via vite.config.ts in the frontend.\n * In packaged mode, connect-src must include ws://127.0.0.1:* for WebSocket.\n */\n\nimport * as FS from \"node:fs\";\nimport * as Path from \"node:path\";\n\nimport { protocol } from \"electron\";\n\nimport { DESKTOP_SCHEME, resolveServerStaticDir } from \"./main.js\";\n\nlet registered = false;\n\n// ─── Path resolution ──────────────────────────────────────────────────────────\n\nexport function resolveStaticPath(staticRoot: string, requestUrl: string): string {\n const fallbackIndex = Path.join(staticRoot, \"index.html\");\n\n let url: URL;\n try {\n url = new URL(requestUrl);\n } catch {\n return fallbackIndex;\n }\n\n const rawPath = decodeURIComponent(url.pathname);\n const normalized = Path.posix.normalize(rawPath).replace(/^\\/+/, \"\");\n\n if (normalized.includes(\"..\")) {\n return fallbackIndex;\n }\n\n const requestedPath = normalized.length > 0 ? normalized : \"index.html\";\n const resolved = Path.resolve(staticRoot, requestedPath);\n\n const staticRootResolved = Path.resolve(staticRoot);\n const inRoot =\n resolved === staticRootResolved ||\n resolved.startsWith(staticRootResolved + Path.sep);\n\n if (!inRoot) return fallbackIndex;\n\n // If path has a file extension, serve it (or 404)\n if (Path.extname(resolved)) {\n return resolved;\n }\n\n // SPA route: check for index.html in subdirectory, else root index\n const nestedIndex = Path.join(resolved, \"index.html\");\n if (FS.existsSync(nestedIndex)) return nestedIndex;\n\n return fallbackIndex;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Register the overdeck:// protocol handler.\n * Must be called after app.ready (but registration via registerSchemesAsPrivileged\n * must happen before app.ready — done in main.ts).\n */\nexport function registerDesktopProtocol(): void {\n if (registered) return;\n\n const staticRoot = resolveServerStaticDir();\n if (!staticRoot) {\n console.error(\n \"[desktop/protocol] Static bundle not found — packaged frontend assets missing. \" +\n \"Run 'npm run build:dashboard' first.\",\n );\n return;\n }\n\n const fallbackIndex = Path.join(staticRoot, \"index.html\");\n\n protocol.registerFileProtocol(DESKTOP_SCHEME, (request, callback) => {\n const candidate = resolveStaticPath(staticRoot, request.url);\n const hasExt = Path.extname(candidate).length > 0;\n\n if (!FS.existsSync(candidate)) {\n if (hasExt) {\n // Asset not found — return 404\n callback({ error: -6 /* net::ERR_FILE_NOT_FOUND */ });\n } else {\n // SPA route — fall back to index\n callback({ path: fallbackIndex });\n }\n return;\n }\n\n callback({ path: candidate });\n });\n\n registered = true;\n console.log(`[desktop/protocol] registered ${DESKTOP_SCHEME}:// serving from ${staticRoot}`);\n}\n","import * as FS from \"node:fs\";\nimport * as OS from \"node:os\";\nimport * as Path from \"node:path\";\n\nimport {\n app,\n BrowserWindow,\n ipcMain,\n protocol,\n shell,\n} from \"electron\";\n\nimport { createTray, destroyTray } from \"./tray.js\";\nimport { loadDesktopSettings, getDesktopSettings, updateDesktopSetting } from \"./settings.js\";\nimport { startServer, stopServer } from \"./server.js\";\nimport { configureApplicationMenu } from \"./menu.js\";\nimport { initializeNotifications, registerNotificationHandlers } from \"./notifications.js\";\nimport { handleAutoStartNag } from \"./autostart.js\";\nimport { registerDesktopProtocol } from \"./protocol.js\";\nimport { initializeAutoUpdater, checkForUpdates, downloadUpdate, quitAndInstall, getUpdateStatus, onUpdateStatusChange } from \"./updater.js\";\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\nconst ROOT_DIR = Path.resolve(__dirname, \"../../..\");\nconst isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);\nconst APP_DISPLAY_NAME = isDevelopment ? \"Overdeck (Dev)\" : \"Overdeck\";\nconst APP_ID = \"com.overdeck.app\";\nconst LINUX_WM_CLASS = isDevelopment ? \"overdeck-dev\" : \"overdeck\";\nexport const DESKTOP_SCHEME = \"overdeck\";\n\n// IPC channel names\nexport const IPC = {\n GET_SERVER_URL: \"pan:get-server-url\",\n GET_WS_URL: \"pan:get-ws-url\",\n PICK_FOLDER: \"pan:pick-folder\",\n OPEN_EXTERNAL: \"pan:open-external\",\n OPEN_TERMINAL_WINDOW: \"pan:open-terminal-window\",\n SET_ALWAYS_ON_TOP: \"pan:set-always-on-top\",\n MENU_ACTION: \"pan:menu-action\",\n GET_DESKTOP_SETTINGS: \"pan:get-desktop-settings\",\n UPDATE_DESKTOP_SETTING: \"pan:update-desktop-setting\",\n NOTIFY: \"pan:notify\",\n GET_UPDATE_STATUS: \"pan:get-update-status\",\n CHECK_FOR_UPDATES: \"pan:check-for-updates\",\n DOWNLOAD_UPDATE: \"pan:download-update\",\n QUIT_AND_INSTALL: \"pan:quit-and-install\",\n RESTART_DASHBOARD: \"pan:restart-dashboard\",\n} as const;\n\n// ─── State ────────────────────────────────────────────────────────────────────\n\nlet mainWindow: BrowserWindow | null = null;\nexport let serverPort = 0;\nexport let serverUrl = \"\";\nexport let serverWsUrl = \"\";\nexport let isQuitting = false;\nconst terminalWindows = new Map<string, BrowserWindow>();\n\n// ─── Single instance lock ─────────────────────────────────────────────────────\n// A second launch must not boot another full app (window, tray, embedded\n// server). Duplicates exit immediately and the running instance is focused\n// via the second-instance event below (PAN-2559).\n\nif (!app.requestSingleInstanceLock()) {\n app.exit(0);\n}\n\napp.on(\"second-instance\", () => showOrCreateWindow());\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nexport function resolveResourcePath(fileName: string): string | null {\n const candidates = [\n // Packaged (electron-builder): resources are in process.resourcesPath/resources/\n Path.join(process.resourcesPath ?? \"\", \"resources\", fileName),\n // npx / global-install: resources are bundled at <package>/resources/\n Path.join(__dirname, \"../resources\", fileName),\n // Dev: resources are in the monorepo\n Path.join(ROOT_DIR, \"apps/desktop/resources\", fileName),\n ];\n for (const candidate of candidates) {\n if (FS.existsSync(candidate)) return candidate;\n }\n return null;\n}\n\nexport function resolveServerEntry(): string {\n if (app.isPackaged) {\n return Path.join(process.resourcesPath ?? \"\", \"server/server.js\");\n }\n // npx / global-install: server bundle is at <package>/server/server.js\n const npxBundle = Path.join(__dirname, \"../server/server.js\");\n if (FS.existsSync(npxBundle)) return npxBundle;\n // Dev mode: server is built in the monorepo root\n return Path.join(ROOT_DIR, \"dist/dashboard/server.js\");\n}\n\nexport function resolveServerStaticDir(): string | null {\n const candidates: string[] = [];\n if (app.isPackaged) {\n candidates.push(Path.join(process.resourcesPath ?? \"\", \"server/public\"));\n } else {\n // npx / global-install: static assets are at <package>/server/public\n candidates.push(Path.join(__dirname, \"../server/public\"));\n // Dev mode fallback\n candidates.push(Path.join(ROOT_DIR, \"dist/dashboard/public\"));\n }\n for (const candidate of candidates) {\n if (FS.existsSync(Path.join(candidate, \"index.html\"))) {\n return candidate;\n }\n }\n return null;\n}\n\nexport function resolveWindowUrl(): string {\n if (isDevelopment) {\n return process.env.VITE_DEV_SERVER_URL!;\n }\n // Serve the window from the embedded server's HTTP origin so the frontend's\n // relative /api fetches and /ws/* sockets are same-origin, exactly like the\n // browser dashboard. The overdeck:// protocol page has no working data\n // layer (relative fetches resolve to the app bundle, PAN-2561) — it is only\n // the pre-server fallback.\n if (serverUrl) {\n return serverUrl;\n }\n return `${DESKTOP_SCHEME}://app/index.html`;\n}\n\nfunction createTerminalWindow(sessionName: string, title: string): BrowserWindow {\n const win = new BrowserWindow({\n width: 900,\n height: 650,\n title,\n webPreferences: {\n preload: Path.join(__dirname, \"preload.js\"),\n contextIsolation: true,\n nodeIntegration: false,\n sandbox: false,\n },\n show: false,\n });\n\n win.once(\"ready-to-show\", () => win.show());\n void win.loadURL(`${resolveWindowUrl()}?terminal=${encodeURIComponent(sessionName)}&title=${encodeURIComponent(title)}`);\n return win;\n}\n\n// ─── IPC: basic handlers ──────────────────────────────────────────────────────\n\nfunction registerIpcHandlers(): void {\n ipcMain.on(IPC.GET_SERVER_URL, (event) => {\n event.returnValue = serverUrl;\n });\n\n ipcMain.on(IPC.GET_WS_URL, (event) => {\n event.returnValue = serverWsUrl;\n });\n\n ipcMain.handle(IPC.PICK_FOLDER, async () => {\n const { dialog } = await import(\"electron\");\n const result = await dialog.showOpenDialog({ properties: [\"openDirectory\"] });\n return result.canceled ? null : (result.filePaths[0] ?? null);\n });\n\n ipcMain.handle(IPC.OPEN_EXTERNAL, async (_event, url: unknown) => {\n if (typeof url !== \"string\") return;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return;\n await shell.openExternal(parsed.toString());\n });\n\n ipcMain.handle(IPC.GET_DESKTOP_SETTINGS, () => getDesktopSettings());\n\n ipcMain.on(IPC.OPEN_TERMINAL_WINDOW, (_event, sessionName: unknown, title: unknown) => {\n if (typeof sessionName !== \"string\" || typeof title !== \"string\") return;\n\n const existing = terminalWindows.get(sessionName);\n if (existing && !existing.isDestroyed()) {\n if (!existing.isVisible()) existing.show();\n existing.focus();\n return;\n }\n\n const win = createTerminalWindow(sessionName, title);\n terminalWindows.set(sessionName, win);\n win.on(\"closed\", () => terminalWindows.delete(sessionName));\n });\n\n ipcMain.on(IPC.SET_ALWAYS_ON_TOP, (_event, value: unknown) => {\n const focused = BrowserWindow.getFocusedWindow();\n if (focused) {\n focused.setAlwaysOnTop(value === true);\n }\n });\n\n ipcMain.handle(IPC.UPDATE_DESKTOP_SETTING, (_event, key: unknown, value: unknown) => {\n if (typeof key !== \"string\") return;\n const updated = updateDesktopSetting(key, value);\n // Apply auto-start setting immediately\n if (updated && key === \"autoStart.enabled\") {\n app.setLoginItemSettings({ openAtLogin: value === true });\n }\n });\n\n // Update IPC handlers\n ipcMain.handle(IPC.GET_UPDATE_STATUS, () => getUpdateStatus());\n\n ipcMain.handle(IPC.CHECK_FOR_UPDATES, async () => {\n try {\n await checkForUpdates();\n } catch (err) {\n console.error(\"[main] checkForUpdates failed:\", err);\n }\n return getUpdateStatus();\n });\n\n ipcMain.handle(IPC.DOWNLOAD_UPDATE, async () => {\n try {\n await downloadUpdate();\n } catch (err) {\n console.error(\"[main] downloadUpdate failed:\", err);\n }\n return getUpdateStatus();\n });\n\n ipcMain.on(IPC.QUIT_AND_INSTALL, () => {\n quitAndInstall();\n });\n\n ipcMain.handle(IPC.RESTART_DASHBOARD, async () => {\n console.log(\"[main] manual dashboard restart requested\");\n stopServer();\n // Brief delay so SIGTERM has a chance to land before we re-spawn.\n await new Promise((resolve) => setTimeout(resolve, 500));\n startServer((port, wsUrl) => {\n serverPort = port;\n serverUrl = `http://127.0.0.1:${port}`;\n serverWsUrl = wsUrl;\n console.log(`[main] dashboard restarted on ${serverUrl}`);\n });\n });\n}\n\n// ─── Window ───────────────────────────────────────────────────────────────────\n\nexport function createWindow(): BrowserWindow {\n const win = new BrowserWindow({\n width: 1400,\n height: 900,\n minWidth: 900,\n minHeight: 600,\n title: APP_DISPLAY_NAME,\n webPreferences: {\n preload: Path.join(__dirname, \"preload.js\"),\n contextIsolation: true,\n nodeIntegration: false,\n sandbox: false,\n },\n show: false,\n });\n\n win.once(\"ready-to-show\", () => win.show());\n\n win.on(\"close\", (event) => {\n if (!isQuitting && process.platform === \"darwin\") {\n event.preventDefault();\n win.hide();\n }\n });\n\n void win.loadURL(resolveWindowUrl());\n return win;\n}\n\nexport function showOrCreateWindow(): void {\n const existing =\n BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0];\n if (existing) {\n if (!existing.isVisible()) existing.show();\n existing.focus();\n return;\n }\n mainWindow = createWindow();\n}\n\nexport function dispatchMenuAction(action: string): void {\n const win = BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0];\n if (!win) {\n showOrCreateWindow();\n setTimeout(() => dispatchMenuAction(action), 500);\n return;\n }\n const send = () => {\n if (win.isDestroyed()) return;\n win.webContents.send(IPC.MENU_ACTION, action);\n if (!win.isVisible()) win.show();\n win.focus();\n };\n if (win.webContents.isLoadingMainFrame()) {\n win.webContents.once(\"did-finish-load\", send);\n } else {\n send();\n }\n}\n\n// ─── Server API helper ────────────────────────────────────────────────────────\n\nexport function callServerApi(path: string, method: string): void {\n if (!serverUrl) return;\n fetch(`${serverUrl}${path}`, { method }).catch((err: unknown) => {\n console.error(\"[desktop] server API call failed:\", err);\n });\n}\n\n// ─── Protocol registration (before app.ready) ─────────────────────────────────\n\nprotocol.registerSchemesAsPrivileged([\n {\n scheme: DESKTOP_SCHEME,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n },\n },\n]);\n\nif (process.platform === \"linux\") {\n app.commandLine.appendSwitch(\"class\", LINUX_WM_CLASS);\n}\n\n// ─── App Lifecycle ────────────────────────────────────────────────────────────\n\napp.on(\"ready\", () => {\n loadDesktopSettings();\n registerIpcHandlers();\n registerNotificationHandlers();\n initializeNotifications();\n configureApplicationMenu();\n registerDesktopProtocol();\n const updateChannel = app.getVersion().includes(\"-canary\") ? \"beta\" : \"latest\";\n initializeAutoUpdater(updateChannel);\n\n if (process.platform === \"win32\") {\n app.setAppUserModelId(APP_ID);\n }\n\n if (process.platform === \"darwin\" && app.dock) {\n const iconPath = resolveResourcePath(\"icon.png\");\n if (iconPath) app.dock.setIcon(iconPath);\n }\n\n createTray();\n\n startServer((port, wsUrl) => {\n serverPort = port;\n serverUrl = `http://127.0.0.1:${port}`;\n serverWsUrl = wsUrl;\n // The ready callback fires again after every server crash-restart. One\n // window per callback multiplied windows under a crash loop (PAN-2570) —\n // reuse the existing window and repoint it at the current server URL.\n if (mainWindow && !mainWindow.isDestroyed()) {\n void mainWindow.loadURL(resolveWindowUrl());\n return;\n }\n mainWindow = createWindow();\n handleAutoStartNag();\n });\n});\n\napp.on(\"window-all-closed\", () => {\n if (process.platform !== \"darwin\") {\n isQuitting = true;\n app.quit();\n }\n});\n\napp.on(\"activate\", () => showOrCreateWindow());\n\napp.on(\"before-quit\", () => {\n isQuitting = true;\n destroyTray();\n stopServer();\n});\n"],"mappings":"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"}
|