feat(desktop): add startup and onboarding flow

Add phase-based desktop boot progress, fresh-install sandbox testing, and first-run provider credential onboarding so packaged installs can start cleanly without manual settings detours.
This commit is contained in:
Brooklyn Nicholson
2026-05-07 22:33:44 -04:00
parent fc9d18b03f
commit 89d5ee4b10
17 changed files with 1056 additions and 145 deletions
@@ -3,6 +3,8 @@ import { useQueryClient } from '@tanstack/react-query'
import { lazy, Suspense, useCallback, useEffect, useRef } from 'react'
import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom'
import { DesktopBootOverlay } from '@/components/desktop-boot-overlay'
import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay'
import { Pane, PaneMain } from '@/components/pane-shell'
import { useSkinCommand } from '@/themes/use-skin-command'
@@ -395,6 +397,17 @@ export function DesktopController() {
const overlays = (
<>
<DesktopBootOverlay />
<DesktopOnboardingOverlay
enabled={gatewayState === 'open'}
onCompleted={() => {
void refreshHermesConfig()
void refreshCurrentModel()
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
}}
onOpenSettings={openSettings}
requestGateway={requestGateway}
/>
<ModelPickerOverlay gateway={gatewayRef.current || undefined} onSelect={selectModel} />
{settingsOpen && (
@@ -1,6 +1,13 @@
import { useEffect, useRef } from 'react'
import { HermesGateway } from '@/hermes'
import {
$desktopBoot,
applyDesktopBootProgress,
completeDesktopBoot,
failDesktopBoot,
setDesktopBootStep
} from '@/store/boot'
import { setGateway } from '@/store/gateway'
import { notify, notifyError } from '@/store/notifications'
import { setConnection, setGatewayState, setSessionsLoading } from '@/store/session'
@@ -44,11 +51,24 @@ export function useGatewayBoot({
const desktop = window.hermesDesktop
if (!desktop) {
failDesktopBoot('Desktop IPC bridge is unavailable.')
setSessionsLoading(false)
return () => void (cancelled = true)
}
const offBootProgress = desktop.onBootProgress(payload => applyDesktopBootProgress(payload))
void desktop
.getBootProgress()
.then(snapshot => applyDesktopBootProgress(snapshot))
.catch(() => undefined)
setDesktopBootStep({
phase: 'renderer.boot',
message: 'Starting desktop connection',
progress: 6
})
const gateway = new HermesGateway()
callbacksRef.current.onGatewayReady(gateway)
setGateway(gateway)
@@ -57,6 +77,10 @@ export function useGatewayBoot({
const offEvent = gateway.onEvent(event => callbacksRef.current.handleGatewayEvent(event))
const offExit = desktop.onBackendExit(() => {
if ($desktopBoot.get().running || $desktopBoot.get().visible) {
failDesktopBoot('Hermes background process exited during startup.')
}
notify({
kind: 'error',
title: 'Backend stopped',
@@ -73,6 +97,11 @@ export function useGatewayBoot({
return
}
setDesktopBootStep({
phase: 'renderer.gateway.connect',
message: 'Connecting live desktop gateway',
progress: 95
})
callbacksRef.current.onConnectionReady(conn)
setConnection(conn)
await gateway.connect(conn.wsUrl)
@@ -81,15 +110,28 @@ export function useGatewayBoot({
return
}
setDesktopBootStep({
phase: 'renderer.config',
message: 'Loading Hermes settings',
progress: 97
})
await callbacksRef.current.refreshHermesConfig()
if (cancelled) {
return
}
setDesktopBootStep({
phase: 'renderer.sessions',
message: 'Loading recent sessions',
progress: 99
})
await callbacksRef.current.refreshSessions()
completeDesktopBoot()
} catch (err) {
if (!cancelled) {
const message = err instanceof Error ? err.message : String(err)
failDesktopBoot(message)
notifyError(err, 'Desktop boot failed')
setSessionsLoading(false)
}
@@ -103,6 +145,7 @@ export function useGatewayBoot({
offState()
offEvent()
offExit()
offBootProgress()
gateway.close()
callbacksRef.current.onConnectionReady(null)
callbacksRef.current.onGatewayReady(null)