import { useStore } from '@nanostores/react' import clsx from 'clsx' import { Check, ChevronRight, FileText, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { BrandMark } from '../components/brand-mark' import { Button } from '../components/button' import { Loader } from '../components/loader' import { formatDuration, formatElapsed } from '../lib/format' import { $mode, $progress, type BootstrapStateModel, cancelInstall, type StageState } from '../store' interface ProgressProps { bootstrap: BootstrapStateModel } /* * Progress screen — drives a stage list + collapsible log panel. Uses * the DS for the top bar so its motion + ring match the rest * of the product. */ export default function ProgressScreen({ bootstrap }: ProgressProps) { const progress = useStore($progress) const mode = useStore($mode) const [showLogs, setShowLogs] = useState(false) const [now, setNow] = useState(() => Date.now()) const logEndRef = useRef(null) useEffect(() => { if (showLogs && logEndRef.current) { logEndRef.current.scrollIntoView({ behavior: 'smooth' }) } }, [bootstrap.logs.length, showLogs]) // Tick once a second while the run is in flight so the active step shows a // live elapsed timer — a long single step (e.g. the dependency download) // reads as working, not frozen. Stops when nothing is running. useEffect(() => { if (bootstrap.status !== 'running') { return } const id = window.setInterval(() => setNow(Date.now()), 1000) return () => window.clearInterval(id) }, [bootstrap.status]) const isUpdate = mode === 'update' const title = bootstrap.status === 'completed' ? 'Done' : isUpdate ? 'Updating Hermes' : 'Setting up Hermes Agent' const description = isUpdate ? 'Hermes is updating to the latest version — this only takes a moment.' : 'This is a one-time setup. The Hermes installer is downloading dependencies and configuring your machine. Subsequent launches will skip this step.' const pct = Math.round(progress.fraction * 100) return (
{/* Header: brand + title + description, matching the desktop install overlay. */}

{title}

{description}

{/* Progress line + bar; the count shimmers while the install runs. pt-2 matches the log header's py-2 so the "steps complete" line and the "Live output" header share a baseline. */}
{progress.done} of {progress.total} steps complete {pct}%
{/* Flat stage list: only the running step is opaque; the rest read as muted. Running loader overhangs left so labels stay aligned; the terminal check/cross sits right of the label. */}
    {bootstrap.stageOrder.map((name) => { const rec = bootstrap.stages[name] if (!rec) {return null} const meta = rec.state === 'running' && rec.startedAt != null ? formatElapsed(now - rec.startedAt) : rec.durationMs != null && rec.state !== 'failed' ? formatDuration(rec.durationMs) : null return (
  1. {rec.state === 'running' && } {rec.info.title} {meta && {meta}}
  2. ) })}
{showLogs && (
Live output {bootstrap.logs.length} lines
{bootstrap.logs.map((entry, idx) => (
{entry.line}
))}
)}
{bootstrap.status === 'running' && ( )}
) } // Terminal-state markers, neutral by design: a muted check for done/skipped // (no celebratory green), a destructive cross for failure. Running renders its // spinner on the left; pending stays icon-less. function StateIcon({ state }: { state: StageState | null }) { if (state === 'succeeded') { return } if (state === 'skipped') { return } if (state === 'failed') { return } return null }