@@ -237,6 +237,33 @@ async fn restart_core_process(
237237 state. inner ( ) . restart ( ) . await
238238}
239239
240+ /// Start the embedded core process on demand.
241+ ///
242+ /// Called by the BootCheckGate (Local mode) before the version check. The
243+ /// core no longer auto-spawns at Tauri setup — the UI is responsible for
244+ /// driving the lifecycle so it can surface startup failures and version
245+ /// mismatches to the user.
246+ ///
247+ /// Idempotent: `ensure_running` is a no-op if the core is already up.
248+ #[ tauri:: command]
249+ async fn start_core_process (
250+ state : tauri:: State < ' _ , core_process:: CoreProcessHandle > ,
251+ ) -> Result < ( ) , String > {
252+ log:: info!( "[core] start_core_process: command invoked from frontend" ) ;
253+ state. inner ( ) . ensure_running ( ) . await
254+ }
255+
256+ /// Cleanly exit the application.
257+ ///
258+ /// Called by the BootCheckGate "Quit" button when the core is unreachable and
259+ /// the user chooses to close the app rather than switch modes.
260+ #[ tauri:: command]
261+ async fn app_quit ( app : tauri:: AppHandle < AppRuntime > ) -> Result < ( ) , String > {
262+ log:: info!( "[app] app_quit: quit requested from frontend" ) ;
263+ app. exit ( 0 ) ;
264+ Ok ( ( ) )
265+ }
266+
240267#[ tauri:: command]
241268async fn restart_app ( app : tauri:: AppHandle < AppRuntime > ) -> Result < ( ) , String > {
242269 log:: info!( "[app] restart_app invoked from frontend" ) ;
@@ -1327,7 +1354,6 @@ pub fn run() {
13271354 return Err ( "webview_apis bridge failed to start — aborting setup" . into ( ) ) ;
13281355 }
13291356
1330- let _ = daemon_mode;
13311357 let core_handle =
13321358 core_process:: CoreProcessHandle :: new ( core_process:: default_core_port ( ) ) ;
13331359 std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , core_handle. rpc_url ( ) ) ;
@@ -1352,13 +1378,23 @@ pub fn run() {
13521378 }
13531379
13541380 app. manage ( core_handle. clone ( ) ) ;
1355- tauri:: async_runtime:: spawn ( async move {
1356- if let Err ( err) = core_handle. ensure_running ( ) . await {
1357- log:: error!( "[core] failed to start embedded core: {err}" ) ;
1358- return ;
1359- }
1360- log:: info!( "[core] embedded core ready" ) ;
1361- } ) ;
1381+ // NOTE: the core is NOT auto-spawned here. The BootCheckGate UI
1382+ // calls `start_core_process` (Local mode) after the user picks a
1383+ // mode, which lets the frontend surface startup failures and
1384+ // version mismatches before the rest of the app mounts.
1385+ //
1386+ // In daemon mode (headless) we spawn immediately so the tray
1387+ // agent is available without waiting for a UI interaction.
1388+ if daemon_mode {
1389+ let core_handle_daemon = core_handle. clone ( ) ;
1390+ tauri:: async_runtime:: spawn ( async move {
1391+ if let Err ( err) = core_handle_daemon. ensure_running ( ) . await {
1392+ log:: error!( "[core] daemon_mode — failed to start embedded core: {err}" ) ;
1393+ return ;
1394+ }
1395+ log:: info!( "[core] daemon_mode — embedded core ready" ) ;
1396+ } ) ;
1397+ }
13621398
13631399 // Restore last-known window position+size before showing the
13641400 // window so the user's first paint after a restart-driven flow
@@ -1665,6 +1701,8 @@ pub fn run() {
16651701 download_app_update,
16661702 install_app_update,
16671703 restart_core_process,
1704+ start_core_process,
1705+ app_quit,
16681706 restart_app,
16691707 get_active_user_id,
16701708 schedule_cef_profile_purge,
0 commit comments