@@ -38,8 +38,18 @@ const { killProcessTree } = require("./processTree");
3838const { resolveServerEntry } = require ( "./lib/resolveServerEntry" ) ;
3939const { resolveDarwinHelperExecutable } = require ( "./lib/resolveNodeHelper" ) ;
4040const { resolveRemoteServerUrl, isValidHttpUrl } = require ( "./lib/resolveRemoteServerUrl" ) ;
41- const { writeRemoteServerUrl } = require ( "./lib/remoteServerPreferences" ) ;
41+ const {
42+ readPreferences,
43+ writeRemoteServerUrl,
44+ writeCloseBehavior,
45+ } = require ( "./lib/remoteServerPreferences" ) ;
4246const { buildReadinessUrl, waitForServer } = require ( "./lib/serverReadiness" ) ;
47+ const {
48+ CLOSE_BEHAVIOR_KEEP_LOADED ,
49+ CLOSE_BEHAVIOR_UNLOAD ,
50+ normalizeCloseBehavior,
51+ resolveRendererUrl,
52+ } = require ( "./lib/windowClosePolicy" ) ;
4353
4454// ── Single Instance Lock ───────────────────────────────────
4555const gotTheLock = app . requestSingleInstanceLock ( ) ;
@@ -49,11 +59,12 @@ if (!gotTheLock) {
4959}
5060
5161app . on ( "second-instance" , ( ) => {
52- if ( mainWindow ) {
53- if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
54- mainWindow . show ( ) ;
55- mainWindow . focus ( ) ;
56- }
62+ const isHeadless =
63+ process . argv . includes ( "--headless" ) ||
64+ process . argv . includes ( "--cli" ) ||
65+ process . env . OMNIROUTE_HEADLESS === "true" ;
66+ if ( isHeadless ) return ;
67+ showMainWindow ( ) ;
5768} ) ;
5869
5970// ── Environment Detection ──────────────────────────────────
@@ -71,6 +82,7 @@ let nextServer = null;
7182let serverPort = 20128 ;
7283let isServerStopped = false ;
7384let remoteServerPromptWindow = null ;
85+ let lastRendererUrl = null ;
7486
7587// ── Remote Server Mode ──────────────────────────────────────
7688// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
@@ -81,6 +93,8 @@ const REMOTE_SERVER_PREFS_PATH = path.join(
8193 resolveDataDir ( null , process . env ) ,
8294 "electron-preferences.json"
8395) ;
96+ const electronPreferences = readPreferences ( REMOTE_SERVER_PREFS_PATH ) ;
97+ let closeBehavior = electronPreferences . closeBehavior ;
8498let remoteServerUrl = resolveRemoteServerUrl ( {
8599 env : process . env ,
86100 prefsPath : REMOTE_SERVER_PREFS_PATH ,
@@ -365,14 +379,18 @@ function setupContentSecurityPolicy() {
365379}
366380
367381// ── Create Window ──────────────────────────────────────────
368- function createWindow ( ) {
382+ function createWindow ( { showWhenReady = true } = { } ) {
383+ if ( mainWindow && ! mainWindow . isDestroyed ( ) ) return mainWindow ;
384+
385+ const rendererStartedAt = Date . now ( ) ;
386+
369387 // Platform-conditional options (#9)
370388 const platformWindowOptions =
371389 process . platform === "darwin"
372390 ? { titleBarStyle : "hiddenInset" , trafficLightPosition : { x : 16 , y : 16 } }
373391 : { titleBarStyle : "default" } ;
374392
375- mainWindow = new BrowserWindow ( {
393+ const window = new BrowserWindow ( {
376394 width : 1400 ,
377395 height : 900 ,
378396 minWidth : 1024 ,
@@ -390,28 +408,25 @@ function createWindow() {
390408 backgroundColor : "#0a0a0a" ,
391409 ...platformWindowOptions ,
392410 } ) ;
411+ mainWindow = window ;
393412
394413 // Load the Next.js app
395- mainWindow . loadURL ( getServerUrl ( ) ) ;
414+ window . loadURL ( resolveRendererUrl ( lastRendererUrl , getServerUrl ( ) ) ) ;
396415 if ( isDev ) {
397- mainWindow . webContents . openDevTools ( { mode : "detach" } ) ;
416+ window . webContents . openDevTools ( { mode : "detach" } ) ;
398417 }
399418
400- // Show window when ready (unless starting minimized/hidden in tray)
401- mainWindow . once ( "ready-to-show" , ( ) => {
402- const startHidden =
403- process . argv . includes ( "--hidden" ) ||
404- process . argv . includes ( "--minimized" ) ||
405- app . getLoginItemSettings ( ) . wasOpenedAsHidden ;
406- if ( ! startHidden ) {
407- mainWindow . show ( ) ;
419+ window . once ( "ready-to-show" , ( ) => {
420+ console . log ( `[Electron] Renderer ready in ${ Date . now ( ) - rendererStartedAt } ms` ) ;
421+ if ( showWhenReady ) {
422+ window . show ( ) ;
408423 } else {
409424 console . log ( "[Electron] Launched hidden in background tray" ) ;
410425 }
411426 } ) ;
412427
413428 // Handle external links — validate URL protocol to prevent RCE
414- mainWindow . webContents . setWindowOpenHandler ( ( { url } ) => {
429+ window . webContents . setWindowOpenHandler ( ( { url } ) => {
415430 try {
416431 const parsedUrl = new URL ( url ) ;
417432 if ( [ "http:" , "https:" ] . includes ( parsedUrl . protocol ) ) {
@@ -425,18 +440,46 @@ function createWindow() {
425440 return { action : "deny" } ;
426441 } ) ;
427442
428- // Handle window close — minimize to tray
429- mainWindow . on ( "close" , ( event ) => {
443+ // Keep the server alive while either hiding the renderer for a fast reopen or
444+ // unloading it to reclaim memory, according to the persisted tray preference.
445+ window . on ( "close" , ( event ) => {
430446 if ( ! app . isQuitting ) {
431447 event . preventDefault ( ) ;
432- mainWindow . hide ( ) ;
448+ lastRendererUrl = resolveRendererUrl ( window . webContents . getURL ( ) , getServerUrl ( ) ) ;
449+ if ( closeBehavior === CLOSE_BEHAVIOR_UNLOAD ) {
450+ console . log ( "[Electron] Dashboard renderer unloaded; server remains running" ) ;
451+ window . destroy ( ) ;
452+ } else {
453+ console . log ( "[Electron] Dashboard hidden; renderer kept loaded" ) ;
454+ window . hide ( ) ;
455+ }
433456 }
434457 return false ;
435458 } ) ;
436459
437- mainWindow . on ( "closed" , ( ) => {
438- mainWindow = null ;
460+ window . on ( "closed" , ( ) => {
461+ if ( mainWindow === window ) mainWindow = null ;
439462 } ) ;
463+
464+ return window ;
465+ }
466+
467+ function showMainWindow ( ) {
468+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) {
469+ createWindow ( ) ;
470+ return ;
471+ }
472+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
473+ mainWindow . show ( ) ;
474+ mainWindow . focus ( ) ;
475+ }
476+
477+ function setCloseBehavior ( nextBehavior ) {
478+ const normalized = normalizeCloseBehavior ( nextBehavior ) ;
479+ if ( ! normalized || normalized === closeBehavior ) return ;
480+ closeBehavior = normalized ;
481+ writeCloseBehavior ( REMOTE_SERVER_PREFS_PATH , closeBehavior ) ;
482+ createTray ( ) ;
440483}
441484
442485// ── System Tray ────────────────────────────────────────────
@@ -465,12 +508,7 @@ function createTray() {
465508 const contextMenu = Menu . buildFromTemplate ( [
466509 {
467510 label : "Open OmniRoute" ,
468- click : ( ) => {
469- if ( mainWindow ) {
470- mainWindow . show ( ) ;
471- mainWindow . focus ( ) ;
472- }
473- } ,
511+ click : ( ) => showMainWindow ( ) ,
474512 } ,
475513 {
476514 label : "Open Dashboard" ,
@@ -504,6 +542,23 @@ function createTray() {
504542 } ,
505543 ] ,
506544 } ,
545+ {
546+ label : "When Dashboard Closes" ,
547+ submenu : [
548+ {
549+ label : "Keep Loaded (Faster Reopen)" ,
550+ type : "radio" ,
551+ checked : closeBehavior === CLOSE_BEHAVIOR_KEEP_LOADED ,
552+ click : ( ) => setCloseBehavior ( CLOSE_BEHAVIOR_KEEP_LOADED ) ,
553+ } ,
554+ {
555+ label : "Unload Renderer (Lower Memory)" ,
556+ type : "radio" ,
557+ checked : closeBehavior === CLOSE_BEHAVIOR_UNLOAD ,
558+ click : ( ) => setCloseBehavior ( CLOSE_BEHAVIOR_UNLOAD ) ,
559+ } ,
560+ ] ,
561+ } ,
507562 { type : "separator" } ,
508563 {
509564 label : "Check for Updates" ,
@@ -522,12 +577,7 @@ function createTray() {
522577 tray . setToolTip ( "OmniRoute" ) ;
523578 tray . setContextMenu ( contextMenu ) ;
524579
525- tray . on ( "double-click" , ( ) => {
526- if ( mainWindow ) {
527- mainWindow . show ( ) ;
528- mainWindow . focus ( ) ;
529- }
530- } ) ;
580+ tray . on ( "double-click" , ( ) => showMainWindow ( ) ) ;
531581}
532582
533583// ── Change Port (#3: now restarts server) ──────────────────
@@ -549,6 +599,7 @@ async function changePort(newPort) {
549599 await waitForServer ( getServerReadinessUrl ( ) ) ;
550600
551601 // Reload window and update tray
602+ lastRendererUrl = getServerUrl ( ) ;
552603 if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
553604 mainWindow . loadURL ( getServerUrl ( ) ) ;
554605 }
@@ -613,6 +664,7 @@ async function setRemoteServerUrl(nextUrl) {
613664
614665 remoteServerUrl = normalized ;
615666 writeRemoteServerUrl ( REMOTE_SERVER_PREFS_PATH , remoteServerUrl ) ;
667+ lastRendererUrl = getServerUrl ( ) ;
616668
617669 startNextServer ( ) ;
618670 try {
@@ -1106,7 +1158,11 @@ app.whenReady().then(async () => {
11061158 if ( isHeadless ) {
11071159 console . log ( "[Electron] Headless mode active — UI window and tray icon skipped" ) ;
11081160 } else {
1109- createWindow ( ) ;
1161+ const startHidden =
1162+ process . argv . includes ( "--hidden" ) ||
1163+ process . argv . includes ( "--minimized" ) ||
1164+ app . getLoginItemSettings ( ) . wasOpenedAsHidden ;
1165+ createWindow ( { showWhenReady : ! startHidden } ) ;
11101166 createTray ( ) ;
11111167 }
11121168
@@ -1133,11 +1189,7 @@ app.whenReady().then(async () => {
11331189 // macOS: recreate window when dock icon clicked
11341190 app . on ( "activate" , ( ) => {
11351191 if ( isHeadless ) return ;
1136- if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
1137- createWindow ( ) ;
1138- } else if ( mainWindow ) {
1139- mainWindow . show ( ) ;
1140- }
1192+ showMainWindow ( ) ;
11411193 } ) ;
11421194} ) ;
11431195
@@ -1147,7 +1199,7 @@ app.on("window-all-closed", () => {
11471199 process . argv . includes ( "--headless" ) ||
11481200 process . argv . includes ( "--cli" ) ||
11491201 process . env . OMNIROUTE_HEADLESS === "true" ;
1150- if ( process . platform !== "darwin" && ! isHeadless ) {
1202+ if ( process . platform !== "darwin" && ! isHeadless && closeBehavior !== CLOSE_BEHAVIOR_UNLOAD ) {
11511203 app . quit ( ) ;
11521204 }
11531205} ) ;
0 commit comments