@@ -38,7 +38,17 @@ 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" ) ;
46+ const {
47+ CLOSE_BEHAVIOR_KEEP_LOADED ,
48+ CLOSE_BEHAVIOR_UNLOAD ,
49+ normalizeCloseBehavior,
50+ resolveRendererUrl,
51+ } = require ( "./lib/windowClosePolicy" ) ;
4252
4353// ── Single Instance Lock ───────────────────────────────────
4454const gotTheLock = app . requestSingleInstanceLock ( ) ;
@@ -48,11 +58,12 @@ if (!gotTheLock) {
4858}
4959
5060app . on ( "second-instance" , ( ) => {
51- if ( mainWindow ) {
52- if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
53- mainWindow . show ( ) ;
54- mainWindow . focus ( ) ;
55- }
61+ const isHeadless =
62+ process . argv . includes ( "--headless" ) ||
63+ process . argv . includes ( "--cli" ) ||
64+ process . env . OMNIROUTE_HEADLESS === "true" ;
65+ if ( isHeadless ) return ;
66+ showMainWindow ( ) ;
5667} ) ;
5768
5869// ── Environment Detection ──────────────────────────────────
@@ -70,6 +81,7 @@ let nextServer = null;
7081let serverPort = 20128 ;
7182let isServerStopped = false ;
7283let remoteServerPromptWindow = null ;
84+ let lastRendererUrl = null ;
7385
7486// ── Remote Server Mode ──────────────────────────────────────
7587// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
@@ -80,6 +92,8 @@ const REMOTE_SERVER_PREFS_PATH = path.join(
8092 resolveDataDir ( null , process . env ) ,
8193 "electron-preferences.json"
8294) ;
95+ const electronPreferences = readPreferences ( REMOTE_SERVER_PREFS_PATH ) ;
96+ let closeBehavior = electronPreferences . closeBehavior ;
8397let remoteServerUrl = resolveRemoteServerUrl ( {
8498 env : process . env ,
8599 prefsPath : REMOTE_SERVER_PREFS_PATH ,
@@ -352,14 +366,18 @@ function setupContentSecurityPolicy() {
352366}
353367
354368// ── Create Window ──────────────────────────────────────────
355- function createWindow ( ) {
369+ function createWindow ( { showWhenReady = true } = { } ) {
370+ if ( mainWindow && ! mainWindow . isDestroyed ( ) ) return mainWindow ;
371+
372+ const rendererStartedAt = Date . now ( ) ;
373+
356374 // Platform-conditional options (#9)
357375 const platformWindowOptions =
358376 process . platform === "darwin"
359377 ? { titleBarStyle : "hiddenInset" , trafficLightPosition : { x : 16 , y : 16 } }
360378 : { titleBarStyle : "default" } ;
361379
362- mainWindow = new BrowserWindow ( {
380+ const window = new BrowserWindow ( {
363381 width : 1400 ,
364382 height : 900 ,
365383 minWidth : 1024 ,
@@ -377,28 +395,25 @@ function createWindow() {
377395 backgroundColor : "#0a0a0a" ,
378396 ...platformWindowOptions ,
379397 } ) ;
398+ mainWindow = window ;
380399
381400 // Load the Next.js app
382- mainWindow . loadURL ( getServerUrl ( ) ) ;
401+ window . loadURL ( resolveRendererUrl ( lastRendererUrl , getServerUrl ( ) ) ) ;
383402 if ( isDev ) {
384- mainWindow . webContents . openDevTools ( { mode : "detach" } ) ;
403+ window . webContents . openDevTools ( { mode : "detach" } ) ;
385404 }
386405
387- // Show window when ready (unless starting minimized/hidden in tray)
388- mainWindow . once ( "ready-to-show" , ( ) => {
389- const startHidden =
390- process . argv . includes ( "--hidden" ) ||
391- process . argv . includes ( "--minimized" ) ||
392- app . getLoginItemSettings ( ) . wasOpenedAsHidden ;
393- if ( ! startHidden ) {
394- mainWindow . show ( ) ;
406+ window . once ( "ready-to-show" , ( ) => {
407+ console . log ( `[Electron] Renderer ready in ${ Date . now ( ) - rendererStartedAt } ms` ) ;
408+ if ( showWhenReady ) {
409+ window . show ( ) ;
395410 } else {
396411 console . log ( "[Electron] Launched hidden in background tray" ) ;
397412 }
398413 } ) ;
399414
400415 // Handle external links — validate URL protocol to prevent RCE
401- mainWindow . webContents . setWindowOpenHandler ( ( { url } ) => {
416+ window . webContents . setWindowOpenHandler ( ( { url } ) => {
402417 try {
403418 const parsedUrl = new URL ( url ) ;
404419 if ( [ "http:" , "https:" ] . includes ( parsedUrl . protocol ) ) {
@@ -412,18 +427,46 @@ function createWindow() {
412427 return { action : "deny" } ;
413428 } ) ;
414429
415- // Handle window close — minimize to tray
416- mainWindow . on ( "close" , ( event ) => {
430+ // Keep the server alive while either hiding the renderer for a fast reopen or
431+ // unloading it to reclaim memory, according to the persisted tray preference.
432+ window . on ( "close" , ( event ) => {
417433 if ( ! app . isQuitting ) {
418434 event . preventDefault ( ) ;
419- mainWindow . hide ( ) ;
435+ lastRendererUrl = resolveRendererUrl ( window . webContents . getURL ( ) , getServerUrl ( ) ) ;
436+ if ( closeBehavior === CLOSE_BEHAVIOR_UNLOAD ) {
437+ console . log ( "[Electron] Dashboard renderer unloaded; server remains running" ) ;
438+ window . destroy ( ) ;
439+ } else {
440+ console . log ( "[Electron] Dashboard hidden; renderer kept loaded" ) ;
441+ window . hide ( ) ;
442+ }
420443 }
421444 return false ;
422445 } ) ;
423446
424- mainWindow . on ( "closed" , ( ) => {
425- mainWindow = null ;
447+ window . on ( "closed" , ( ) => {
448+ if ( mainWindow === window ) mainWindow = null ;
426449 } ) ;
450+
451+ return window ;
452+ }
453+
454+ function showMainWindow ( ) {
455+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) {
456+ createWindow ( ) ;
457+ return ;
458+ }
459+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
460+ mainWindow . show ( ) ;
461+ mainWindow . focus ( ) ;
462+ }
463+
464+ function setCloseBehavior ( nextBehavior ) {
465+ const normalized = normalizeCloseBehavior ( nextBehavior ) ;
466+ if ( ! normalized || normalized === closeBehavior ) return ;
467+ closeBehavior = normalized ;
468+ writeCloseBehavior ( REMOTE_SERVER_PREFS_PATH , closeBehavior ) ;
469+ createTray ( ) ;
427470}
428471
429472// ── System Tray ────────────────────────────────────────────
@@ -452,12 +495,7 @@ function createTray() {
452495 const contextMenu = Menu . buildFromTemplate ( [
453496 {
454497 label : "Open OmniRoute" ,
455- click : ( ) => {
456- if ( mainWindow ) {
457- mainWindow . show ( ) ;
458- mainWindow . focus ( ) ;
459- }
460- } ,
498+ click : ( ) => showMainWindow ( ) ,
461499 } ,
462500 {
463501 label : "Open Dashboard" ,
@@ -491,6 +529,23 @@ function createTray() {
491529 } ,
492530 ] ,
493531 } ,
532+ {
533+ label : "When Dashboard Closes" ,
534+ submenu : [
535+ {
536+ label : "Keep Loaded (Faster Reopen)" ,
537+ type : "radio" ,
538+ checked : closeBehavior === CLOSE_BEHAVIOR_KEEP_LOADED ,
539+ click : ( ) => setCloseBehavior ( CLOSE_BEHAVIOR_KEEP_LOADED ) ,
540+ } ,
541+ {
542+ label : "Unload Renderer (Lower Memory)" ,
543+ type : "radio" ,
544+ checked : closeBehavior === CLOSE_BEHAVIOR_UNLOAD ,
545+ click : ( ) => setCloseBehavior ( CLOSE_BEHAVIOR_UNLOAD ) ,
546+ } ,
547+ ] ,
548+ } ,
494549 { type : "separator" } ,
495550 {
496551 label : "Check for Updates" ,
@@ -509,12 +564,7 @@ function createTray() {
509564 tray . setToolTip ( "OmniRoute" ) ;
510565 tray . setContextMenu ( contextMenu ) ;
511566
512- tray . on ( "double-click" , ( ) => {
513- if ( mainWindow ) {
514- mainWindow . show ( ) ;
515- mainWindow . focus ( ) ;
516- }
517- } ) ;
567+ tray . on ( "double-click" , ( ) => showMainWindow ( ) ) ;
518568}
519569
520570// ── Change Port (#3: now restarts server) ──────────────────
@@ -536,6 +586,7 @@ async function changePort(newPort) {
536586 await waitForServer ( getServerUrl ( ) ) ;
537587
538588 // Reload window and update tray
589+ lastRendererUrl = getServerUrl ( ) ;
539590 if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
540591 mainWindow . loadURL ( getServerUrl ( ) ) ;
541592 }
@@ -600,6 +651,7 @@ async function setRemoteServerUrl(nextUrl) {
600651
601652 remoteServerUrl = normalized ;
602653 writeRemoteServerUrl ( REMOTE_SERVER_PREFS_PATH , remoteServerUrl ) ;
654+ lastRendererUrl = getServerUrl ( ) ;
603655
604656 startNextServer ( ) ;
605657 try {
@@ -1085,7 +1137,11 @@ app.whenReady().then(async () => {
10851137 if ( isHeadless ) {
10861138 console . log ( "[Electron] Headless mode active — UI window and tray icon skipped" ) ;
10871139 } else {
1088- createWindow ( ) ;
1140+ const startHidden =
1141+ process . argv . includes ( "--hidden" ) ||
1142+ process . argv . includes ( "--minimized" ) ||
1143+ app . getLoginItemSettings ( ) . wasOpenedAsHidden ;
1144+ createWindow ( { showWhenReady : ! startHidden } ) ;
10891145 createTray ( ) ;
10901146 }
10911147
@@ -1112,11 +1168,7 @@ app.whenReady().then(async () => {
11121168 // macOS: recreate window when dock icon clicked
11131169 app . on ( "activate" , ( ) => {
11141170 if ( isHeadless ) return ;
1115- if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
1116- createWindow ( ) ;
1117- } else if ( mainWindow ) {
1118- mainWindow . show ( ) ;
1119- }
1171+ showMainWindow ( ) ;
11201172 } ) ;
11211173} ) ;
11221174
@@ -1126,7 +1178,7 @@ app.on("window-all-closed", () => {
11261178 process . argv . includes ( "--headless" ) ||
11271179 process . argv . includes ( "--cli" ) ||
11281180 process . env . OMNIROUTE_HEADLESS === "true" ;
1129- if ( process . platform !== "darwin" && ! isHeadless ) {
1181+ if ( process . platform !== "darwin" && ! isHeadless && closeBehavior !== CLOSE_BEHAVIOR_UNLOAD ) {
11301182 app . quit ( ) ;
11311183 }
11321184} ) ;
0 commit comments