@@ -34,7 +34,7 @@ import {
3434 initErrorReporting ,
3535 reportAProblem ,
3636} from "./diagnostics" ;
37- import { sidecarCrashHtml } from "./crash-screen" ;
37+ import { sidecarCrashHtml , startupWindowHtml } from "./crash-screen" ;
3838import { replaceSupervisedDaemonForDesktop } from "./supervised-connection" ;
3939import {
4040 bundledExecutorPath ,
@@ -137,6 +137,9 @@ const webUrlForConnection = (conn: SidecarConnection): string => {
137137 return url . toString ( ) ;
138138} ;
139139
140+ const htmlDataUrl = ( html : string ) : string =>
141+ `data:text/html;charset=utf-8,${ encodeURIComponent ( html ) } ` ;
142+
140143// The supervised daemon (and the desktop sidecar) own this data dir — the same
141144// path the CLI's `executor web`/daemon uses, so desktop and CLI share state.
142145const DESKTOP_DATA_DIR = join ( homedir ( ) , ".executor" ) ;
@@ -216,6 +219,40 @@ const confirmEnableBackgroundService = async (): Promise<boolean> => {
216219 return response === 0 ;
217220} ;
218221
222+ const acceptSupervisedConnection = async (
223+ attached : SidecarConnection ,
224+ ) : Promise < SidecarConnection | null > => {
225+ if ( ! shouldReplaceDaemonForDesktop ( attached ) ) return attached ;
226+ const settings = getServerSettings ( ) ;
227+ return replaceSupervisedDaemonForDesktop ( attached , {
228+ install : ( ) =>
229+ installSupervisedService ( {
230+ port : settings . port ,
231+ dataDir : DESKTOP_DATA_DIR ,
232+ } ) ,
233+ waitForAttach : ( ) => waitForSupervisedAttach ( 30_000 , { port : settings . port } ) ,
234+ attach : attachToSupervisedDaemon ,
235+ onInstallFailure : ( error ) => {
236+ log . warn ( "Failed to replace older supervised daemon; re-checking before falling back" , error ) ;
237+ } ,
238+ } ) ;
239+ } ;
240+
241+ const installSupervisedAndWait = async ( reason : string ) : Promise < SidecarConnection | null > => {
242+ const settings = getServerSettings ( ) ;
243+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: install failure falls back to managed-spawn so the app still launches
244+ try {
245+ await installSupervisedService ( {
246+ port : settings . port ,
247+ dataDir : DESKTOP_DATA_DIR ,
248+ } ) ;
249+ } catch ( error ) {
250+ log . warn ( `Failed to install supervised service after ${ reason } ; using managed sidecar` , error ) ;
251+ return null ;
252+ }
253+ return waitForSupervisedAttach ( 15_000 , { port : settings . port } ) ;
254+ } ;
255+
219256/**
220257 * Resolve a connection to the OS-supervised daemon, installing it on first run
221258 * (with consent). Returns null when supervision is unavailable or the user
@@ -225,23 +262,7 @@ const ensureSupervisedConnection = async (): Promise<SidecarConnection | null> =
225262 // 1. Already running → attach.
226263 const attached = await attachToSupervisedDaemon ( ) ;
227264 if ( attached ) {
228- if ( ! shouldReplaceDaemonForDesktop ( attached ) ) return attached ;
229- const settings = getServerSettings ( ) ;
230- return replaceSupervisedDaemonForDesktop ( attached , {
231- install : ( ) =>
232- installSupervisedService ( {
233- port : settings . port ,
234- dataDir : DESKTOP_DATA_DIR ,
235- } ) ,
236- waitForAttach : ( ) => waitForSupervisedAttach ( 30_000 , { port : settings . port } ) ,
237- attach : attachToSupervisedDaemon ,
238- onInstallFailure : ( error ) => {
239- log . warn (
240- "Failed to replace older supervised daemon; re-checking before falling back" ,
241- error ,
242- ) ;
243- } ,
244- } ) ;
265+ return acceptSupervisedConnection ( attached ) ;
245266 }
246267
247268 // Headless/e2e: the first-run background-service prompt and the systemd/
@@ -254,32 +275,41 @@ const ensureSupervisedConnection = async (): Promise<SidecarConnection | null> =
254275 const status = await supervisedServiceStatus ( ) ;
255276 if ( ! status . supported ) return null ;
256277
257- // 2. Registered but not currently serving → kick it and wait.
278+ // 2. Registered and running, but the first attach missed it. Kick once and
279+ // wait only when the kick command succeeds.
258280 if ( status . registered ) {
259- // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a restart failure just falls through to managed-spawn
281+ if ( status . running ) {
282+ const attachedAfterStatus = await attachToSupervisedDaemon ( ) ;
283+ if ( attachedAfterStatus ) return acceptSupervisedConnection ( attachedAfterStatus ) ;
284+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a restart failure just falls through to managed-spawn
285+ try {
286+ await restartSupervisedService ( ) ;
287+ } catch ( error ) {
288+ log . warn (
289+ "Failed to restart running supervised service after attach failed; using managed sidecar" ,
290+ error ,
291+ ) ;
292+ return null ;
293+ }
294+ return waitForSupervisedAttach ( 15_000 ) ;
295+ }
296+
297+ // 3. Registered but not running usually means a stale plist/unit/task. Try
298+ // restart first, then reinstall the service if the OS manager cannot start it.
299+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: restart failure triggers supervised service self-heal
260300 try {
261301 await restartSupervisedService ( ) ;
262302 } catch ( error ) {
263- log . warn ( "Failed to kickstart supervised service" , error ) ;
303+ log . warn ( "Failed to restart registered supervised service; reinstalling" , error ) ;
304+ return installSupervisedAndWait ( "registered service restart failure" ) ;
264305 }
265306 return waitForSupervisedAttach ( 15_000 ) ;
266307 }
267308
268- // 3 . First run → ask, then install + start. The unit carries no secret; the
309+ // 4 . First run → ask, then install + start. The unit carries no secret; the
269310 // supervised daemon mints/loads its bearer from auth.json under DESKTOP_DATA_DIR.
270311 if ( ! ( await confirmEnableBackgroundService ( ) ) ) return null ;
271- const settings = getServerSettings ( ) ;
272- // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: install failure falls back to managed-spawn so the app still launches
273- try {
274- await installSupervisedService ( {
275- port : settings . port ,
276- dataDir : DESKTOP_DATA_DIR ,
277- } ) ;
278- } catch ( error ) {
279- log . error ( "Failed to install supervised service; using managed sidecar" , error ) ;
280- return null ;
281- }
282- return waitForSupervisedAttach ( 15_000 ) ;
312+ return installSupervisedAndWait ( "first-run prompt" ) ;
283313} ;
284314
285315// Crash monitor for the supervised daemon: the OS service manager restarts it
@@ -308,8 +338,7 @@ const armSupervisedMonitor = () => {
308338 if ( supervisedMonitorMisses < SUPERVISED_MONITOR_MISSES_BEFORE_DOWN ) return ;
309339 if ( ! supervisedDaemonDown && window ) {
310340 supervisedDaemonDown = true ;
311- const html = sidecarCrashHtml ( { reported : errorReportingEnabled } ) ;
312- void window . loadURL ( `data:text/html;charset=utf-8,${ encodeURIComponent ( html ) } ` ) ;
341+ showCrashScreen ( window ) ;
313342 }
314343 return ;
315344 }
@@ -408,14 +437,12 @@ const installDockIcon = () => {
408437 log . info ( `[dock-icon] set to ${ iconPath } (${ image . getSize ( ) . width } ×${ image . getSize ( ) . height } )` ) ;
409438} ;
410439
411- const createWindow = async ( conn : SidecarConnection ) => {
440+ const createMainBrowserWindow = ( options : { readonly show : boolean } ) : BrowserWindow => {
412441 const windowState = windowStateKeeper ( {
413442 defaultWidth : 1280 ,
414443 defaultHeight : 800 ,
415444 } ) ;
416445
417- installBearerAuthHeader ( conn . baseUrl , conn . authToken ) ;
418-
419446 const linuxIcon = resolveLinuxIcon ( ) ;
420447
421448 const window = new BrowserWindow ( {
@@ -431,7 +458,7 @@ const createWindow = async (conn: SidecarConnection) => {
431458 // header, which is offset to clear them (.desktop-macos-titlebar).
432459 minWidth : 768 ,
433460 minHeight : 480 ,
434- show : false ,
461+ show : options . show ,
435462 backgroundColor : "#0a0a0a" ,
436463 autoHideMenuBar : true ,
437464 ...( process . platform === "darwin"
@@ -488,15 +515,45 @@ const createWindow = async (conn: SidecarConnection) => {
488515 return { action : "deny" } ;
489516 } ) ;
490517
518+ return window ;
519+ } ;
520+
521+ const showStartupWindow = async ( ) : Promise < void > => {
522+ const window = liveMainWindow ( ) ?? createMainBrowserWindow ( { show : true } ) ;
523+ if ( window . isMinimized ( ) ) window . restore ( ) ;
524+ if ( ! window . isVisible ( ) ) window . show ( ) ;
525+ window . focus ( ) ;
526+
527+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: local startup HTML should not block service startup
528+ try {
529+ await window . loadURL ( htmlDataUrl ( startupWindowHtml ( ) ) ) ;
530+ } catch ( error ) {
531+ log . warn ( "Failed to load startup window" , error ) ;
532+ }
533+ } ;
534+
535+ const showCrashScreen = ( window : BrowserWindow | null = liveMainWindow ( ) ) : void => {
536+ if ( ! window ) return ;
537+ const html = sidecarCrashHtml ( { reported : errorReportingEnabled } ) ;
538+ void window . loadURL ( htmlDataUrl ( html ) ) ;
539+ } ;
540+
541+ const createWindow = async ( conn : SidecarConnection ) => {
542+ installBearerAuthHeader ( conn . baseUrl , conn . authToken ) ;
543+
544+ const existingWindow = liveMainWindow ( ) ;
545+ const window = existingWindow ?? createMainBrowserWindow ( { show : false } ) ;
546+
491547 // A supervised daemon can pass the health probe and still disappear before
492548 // navigation begins. Treat that as a failed connection instead of leaving the
493549 // user with a visible BrowserWindow that only shows the black background.
494550 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Electron navigation rejects when the sidecar vanishes during startup
495551 try {
496552 await window . loadURL ( webUrlForConnection ( conn ) ) ;
553+ if ( ! window . isDestroyed ( ) && ! window . isVisible ( ) ) window . show ( ) ;
497554 } catch ( error ) {
498555 log . error ( "Failed to load Executor web UI" , error ) ;
499- destroyWindow ( window ) ;
556+ if ( ! existingWindow ) destroyWindow ( window ) ;
500557 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: caller decides whether to fall back or surface startup failure
501558 throw error ;
502559 }
@@ -916,6 +973,7 @@ const runUpdateCheck = async ({ alertOnFail }: UpdateCheckOptions) => {
916973// here, the boot-time update check never runs (it sits after a successful
917974// sidecar start), so a broken app could never self-update its way out.
918975const handleFatalSidecarFailure = async ( error : unknown ) => {
976+ showCrashScreen ( ) ;
919977 if ( app . isPackaged ) {
920978 // Install whatever finishes downloading by the time the user quits the
921979 // failure dialog; if it downloads while the dialog is open, the regular
@@ -1035,17 +1093,15 @@ const installApplicationMenu = () => {
10351093const boot = async ( ) => {
10361094 installDockIcon ( ) ;
10371095 installApplicationMenu ( ) ;
1096+ await showStartupWindow ( ) ;
10381097 setupAutoUpdater ( ) ;
10391098 applyFakeUpdateFromEnv ( ) ;
10401099 registerIpcHandlers ( ) ;
10411100 // A sidecar that dies under a live window would leave the web UI failing
10421101 // every request with no explanation. Swap in the crash screen — its
10431102 // buttons drive the regular preload bridge (restart / export diagnostics).
10441103 onUnexpectedSidecarExit ( ( ) => {
1045- const window = liveMainWindow ( ) ;
1046- if ( ! window ) return ;
1047- const html = sidecarCrashHtml ( { reported : errorReportingEnabled } ) ;
1048- void window . loadURL ( `data:text/html;charset=utf-8,${ encodeURIComponent ( html ) } ` ) ;
1104+ showCrashScreen ( ) ;
10491105 // A crashing sidecar may be a broken release — quietly stage any
10501106 // available update so the install prompt appears on its own (same
10511107 // self-heal as the fatal startup path).
0 commit comments