Skip to content

Commit 33d8aa9

Browse files
authored
feat(boot): gate launch on core mode pick + version check (tinyhumansai#1316)
1 parent ba88d8c commit 33d8aa9

11 files changed

Lines changed: 1609 additions & 24 deletions

File tree

app/src-tauri/src/lib.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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]
241268
async 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,

app/src/App.tsx

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PersistGate } from 'redux-persist/integration/react';
66

77
import AppRoutes from './AppRoutes';
88
import AppUpdatePrompt from './components/AppUpdatePrompt';
9+
import BootCheckGate from './components/BootCheckGate/BootCheckGate';
910
import BottomTabBar from './components/BottomTabBar';
1011
import CommandProvider from './components/commands/CommandProvider';
1112
import ServiceBlockingGate from './components/daemon/ServiceBlockingGate';
@@ -49,22 +50,24 @@ function App() {
4950
)}>
5051
<Provider store={store}>
5152
<PersistGate loading={<PersistRehydrationScreen />} persistor={persistor}>
52-
<CoreStateProvider>
53-
<SocketProvider>
54-
<ChatRuntimeProvider>
55-
<Router>
56-
<CommandProvider>
57-
<ServiceBlockingGate>
58-
<AppShell />
59-
<DictationHotkeyManager />
60-
<LocalAIDownloadSnackbar />
61-
<AppUpdatePrompt />
62-
</ServiceBlockingGate>
63-
</CommandProvider>
64-
</Router>
65-
</ChatRuntimeProvider>
66-
</SocketProvider>
67-
</CoreStateProvider>
53+
<BootCheckGate>
54+
<CoreStateProvider>
55+
<SocketProvider>
56+
<ChatRuntimeProvider>
57+
<Router>
58+
<CommandProvider>
59+
<ServiceBlockingGate>
60+
<AppShell />
61+
<DictationHotkeyManager />
62+
<LocalAIDownloadSnackbar />
63+
<AppUpdatePrompt />
64+
</ServiceBlockingGate>
65+
</CommandProvider>
66+
</Router>
67+
</ChatRuntimeProvider>
68+
</SocketProvider>
69+
</CoreStateProvider>
70+
</BootCheckGate>
6871
</PersistGate>
6972
</Provider>
7073
</Sentry.ErrorBoundary>

0 commit comments

Comments
 (0)