import { useEffect, useMemo, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import type {
DesktopBootstrapEvent,
DesktopBootstrapStageDescriptor,
DesktopBootstrapStageResult,
DesktopBootstrapStageState,
DesktopBootstrapState
} from '@/global'
import { useI18n } from '@/i18n'
import { AlertTriangle, Check, ChevronDown, ChevronRight, Loader2 } from '@/lib/icons'
import { cn } from '@/lib/utils'
/**
* DesktopInstallOverlay
*
* Renders the first-launch install progress for Hermes Agent. Mounted always;
* shows itself only when main.cjs reports an in-flight bootstrap (state.active)
* OR an error from a completed-failed bootstrap (state.error). When the
* bootstrap finishes successfully the overlay fades out and the rest of the
* app (existing onboarding overlay -> main UI) takes over.
*
* Subscribes to two channels:
* - getBootstrapState() -- initial snapshot on mount
* - onBootstrapEvent(callback) -- live event stream
*
* The reducer is intentionally simple: every event mutates an in-component
* snapshot the same way main.cjs mutates its server-side snapshot. We don't
* try to reconcile -- if we miss an event (shouldn't happen) the initial
* getBootstrapState() call will resync the picture on the next render.
*
* Stages flagged needs_user_input render with a deliberately subdued style:
* they're expected to come back as skipped=true (install.ps1 short-circuits
* them under -NonInteractive). The post-install configuration flow that
* those stages cover (API key, model, persona, gateway autostart) is handled
* by the existing DesktopOnboardingOverlay, NOT by the install overlay.
*/
interface DesktopInstallOverlayProps {
/** When false, the overlay never renders -- useful for dev when we want
* to suppress it entirely. */
enabled?: boolean
}
interface StageRowProps {
descriptor: DesktopBootstrapStageDescriptor
result: DesktopBootstrapStageResult | undefined
isCurrent: boolean
now: number
}
function formatStageName(name: string): string {
// 'system-packages' -> 'System packages'; 'uv' stays 'uv'
if (name.length <= 3) {
return name
}
return name
.split('-')
.map((word, i) => (i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word))
.join(' ')
}
function formatDuration(ms: number | null | undefined): string {
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
return ''
}
if (ms < 1000) {
return `${ms} ms`
}
const s = ms / 1000
if (s < 60) {
return `${s.toFixed(1)}s`
}
const m = Math.floor(s / 60)
const rs = Math.round(s - m * 60)
return `${m}m ${rs}s`
}
// Live elapsed for a running stage, as m:ss (or s for sub-minute).
function formatElapsed(ms: number): string {
const s = Math.max(0, Math.floor(ms / 1000))
if (s < 60) {
return `${s}s`
}
const m = Math.floor(s / 60)
return `${m}:${String(s - m * 60).padStart(2, '0')}`
}
function StageRow({ descriptor, result, isCurrent, now }: StageRowProps) {
const { t } = useI18n()
const copy = t.install
const state: DesktopBootstrapStageState = result?.state || 'pending'
const elapsed =
state === 'running' && typeof result?.startedAt === 'number' ? formatElapsed(now - result.startedAt) : ''
const icon = useMemo(() => {
switch (state) {
case 'running':
return
case 'succeeded':
return
case 'skipped':
return
case 'failed':
return
case 'pending':
default:
return