Improve desktop runtime UX by surfacing inference readiness in gateway status and hardening WSL link opening.

This also stabilizes markdown code/table block spacing and adds root-install guards so desktop dev runs use a healthy workspace dependency tree.
This commit is contained in:
Brooklyn Nicholson
2026-05-15 16:33:04 -05:00
parent d0c20708ce
commit c30550c552
12 changed files with 188 additions and 95 deletions
@@ -2,15 +2,16 @@ import { IconLayoutDashboard } from '@tabler/icons-react'
import { StatusDot, type StatusTone } from '@/components/status-dot'
import { Button } from '@/components/ui/button'
import { Activity, AlertCircle, RefreshCw } from '@/lib/icons'
import { Activity, AlertCircle } from '@/lib/icons'
import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
import { cn } from '@/lib/utils'
import type { StatusResponse } from '@/types/hermes'
interface GatewayMenuPanelProps {
gatewayState: string
inferenceStatus: RuntimeReadinessResult | null
logLines: readonly string[]
onOpenSystem: () => void
onRestart: () => void
restarting: boolean
statusSnapshot: StatusResponse | null
}
@@ -32,44 +33,41 @@ const RUNTIME_BRACKET_RE = /^\[[^\]]+]\s+/
const trimLogLine = (raw: string) => raw.trim().replace(TIMESTAMP_RE, '').replace(RUNTIME_BRACKET_RE, '')
export function GatewayMenuPanel({
gatewayState,
inferenceStatus,
logLines,
onOpenSystem,
onRestart,
restarting,
statusSnapshot
}: GatewayMenuPanelProps) {
const gatewayRunning = Boolean(statusSnapshot?.gateway_running)
const gatewayOpen = gatewayState === 'open'
const inferenceReady = gatewayOpen && inferenceStatus?.ready === true
const connectionLabel = gatewayOpen ? 'Connected' : prettyState(gatewayState || 'offline')
const inferenceLabel = gatewayOpen
? inferenceStatus
? inferenceReady
? 'Inference ready'
: 'Inference not ready'
: 'Checking inference'
: 'Disconnected'
const platforms = Object.entries(statusSnapshot?.gateway_platforms || {}).sort(([l], [r]) => l.localeCompare(r))
const stateLabel = gatewayRunning ? prettyState(statusSnapshot?.gateway_state || 'online') : 'Offline'
const recentLogs = logLines.slice(-5)
return (
<div className="text-sm">
<div className="flex items-center justify-between gap-2 px-3 py-2.5">
<div className="flex min-w-0 items-center gap-2">
{gatewayRunning ? (
{inferenceReady ? (
<Activity className="size-3.5 text-primary" />
) : (
<AlertCircle className="size-3.5 text-destructive" />
<AlertCircle className={cn('size-3.5', gatewayOpen ? 'text-amber-600' : 'text-destructive')} />
)}
<span className="font-medium">Gateway</span>
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<StatusDot tone={gatewayRunning ? 'good' : 'bad'} />
{stateLabel}
<StatusDot tone={inferenceReady ? 'good' : gatewayOpen ? 'warn' : 'bad'} />
{inferenceLabel}
</span>
</div>
<div className="flex items-center">
<Button
aria-label={restarting ? 'Restarting gateway' : 'Restart gateway'}
className="size-7 text-muted-foreground hover:text-foreground"
disabled={restarting}
onClick={onRestart}
size="icon-sm"
title={restarting ? 'Restarting gateway' : 'Restart gateway'}
variant="ghost"
>
<RefreshCw className={cn(restarting && 'animate-spin')} />
</Button>
<Button
aria-label="Open system panel"
className="size-7 text-muted-foreground hover:text-foreground"
@@ -83,6 +81,11 @@ export function GatewayMenuPanel({
</div>
</div>
<div className="border-t border-border/50 px-3 py-2 text-xs text-muted-foreground">
<div>Connection: {connectionLabel}</div>
{inferenceStatus?.reason && <div className="mt-1 line-clamp-3">{inferenceStatus.reason}</div>}
</div>
{recentLogs.length > 0 && (
<div className="border-t border-border/50 px-3 py-2">
<SectionLabel>Recent activity</SectionLabel>
@@ -109,7 +112,7 @@ export function GatewayMenuPanel({
{platforms.length > 0 && (
<div className="border-t border-border/50 px-3 py-2">
<SectionLabel>Platforms</SectionLabel>
<SectionLabel>Messaging platforms</SectionLabel>
<ul className="mt-1.5 space-y-1">
{platforms.map(([name, platform]) => (
<li className="flex items-center justify-between gap-2 text-xs" key={name}>
@@ -1,23 +1,35 @@
import { useEffect, useState } from 'react'
import { getLogs, getStatus } from '@/hermes'
import { evaluateRuntimeReadiness, type RuntimeReadinessResult } from '@/lib/runtime-readiness'
import type { StatusResponse } from '@/types/hermes'
const REFRESH_MS = 15_000
const LOG_TAIL = 12
export function useStatusSnapshot(gatewayState: string | undefined) {
type GatewayRequester = <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
export function useStatusSnapshot(gatewayState: string | undefined, requestGateway: GatewayRequester) {
const [statusSnapshot, setStatusSnapshot] = useState<StatusResponse | null>(null)
const [gatewayLogLines, setGatewayLogLines] = useState<string[]>([])
const [inferenceStatus, setInferenceStatus] = useState<RuntimeReadinessResult | null>(null)
useEffect(() => {
let cancelled = false
const refresh = async () => {
try {
const [next, logs] = await Promise.all([
const [next, logs, inference] = await Promise.all([
getStatus(),
getLogs({ file: 'gateway', lines: LOG_TAIL }).catch(() => ({ lines: [] }))
getLogs({ file: 'gui', lines: LOG_TAIL }).catch(() => ({ lines: [] })),
gatewayState === 'open'
? evaluateRuntimeReadiness(requestGateway).catch(error => ({
checksDisagree: false,
ready: false,
reason: error instanceof Error ? error.message : String(error),
source: 'fallback' as const
}))
: Promise.resolve(null)
])
if (cancelled) {
@@ -26,6 +38,7 @@ export function useStatusSnapshot(gatewayState: string | undefined) {
setStatusSnapshot(next)
setGatewayLogLines(logs.lines.map(line => line.trim()).filter(Boolean))
setInferenceStatus(inference)
} catch {
// Keep last snapshot through transient gateway flaps.
}
@@ -38,7 +51,7 @@ export function useStatusSnapshot(gatewayState: string | undefined) {
cancelled = true
window.clearInterval(timer)
}
}, [gatewayState])
}, [gatewayState, requestGateway])
return { gatewayLogLines, statusSnapshot }
return { gatewayLogLines, inferenceStatus, statusSnapshot }
}
@@ -1,14 +1,13 @@
import { useStore } from '@nanostores/react'
import { useCallback, useMemo, useState } from 'react'
import { useMemo } from 'react'
import type { CommandCenterSection } from '@/app/command-center'
import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel'
import { restartGateway } from '@/hermes'
import { Activity, AlertCircle, Clock, Command, Cpu, FolderOpen, GitBranch, Hash, Loader2, Sparkles } from '@/lib/icons'
import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
import { compactPath, contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
import { cn } from '@/lib/utils'
import { $desktopActionTasks } from '@/store/activity'
import { notify, notifyError } from '@/store/notifications'
import { $previewServerRestartStatus } from '@/store/preview'
import {
$busy,
@@ -36,6 +35,8 @@ interface StatusbarItemsOptions {
extraLeftItems: readonly StatusbarItem[]
extraRightItems: readonly StatusbarItem[]
gatewayLogLines: readonly string[]
gatewayState: string
inferenceStatus: RuntimeReadinessResult | null
openAgents: () => void
openCommandCenterSection: (section: CommandCenterSection) => void
statusSnapshot: StatusResponse | null
@@ -49,6 +50,8 @@ export function useStatusbarItems({
extraLeftItems,
extraRightItems,
gatewayLogLines,
gatewayState,
inferenceStatus,
openAgents,
openCommandCenterSection,
statusSnapshot,
@@ -73,40 +76,17 @@ export function useStatusbarItems({
const contextUsage = useMemo(() => usageContextLabel(currentUsage), [currentUsage])
const contextBar = useMemo(() => contextBarLabel(currentUsage), [currentUsage])
const [restartingGateway, setRestartingGateway] = useState(false)
const handleRestartGateway = useCallback(async () => {
if (restartingGateway) {
return
}
setRestartingGateway(true)
try {
await restartGateway()
notify({
kind: 'success',
title: 'Gateway restart requested',
message: 'Status will update once the gateway reconnects.'
})
} catch (err) {
notifyError(err, 'Failed to restart gateway')
} finally {
setRestartingGateway(false)
}
}, [restartingGateway])
const gatewayMenuContent = useMemo(
() => (
<GatewayMenuPanel
logLines={gatewayLogLines}
onOpenSystem={() => openCommandCenterSection('system')}
onRestart={() => void handleRestartGateway()}
restarting={restartingGateway}
gatewayState={gatewayState}
inferenceStatus={inferenceStatus}
statusSnapshot={statusSnapshot}
/>
),
[gatewayLogLines, handleRestartGateway, openCommandCenterSection, restartingGateway, statusSnapshot]
[gatewayLogLines, gatewayState, inferenceStatus, openCommandCenterSection, statusSnapshot]
)
const { bgFailed, bgRunning, subagentsRunning } = useMemo(() => {
@@ -124,7 +104,22 @@ export function useStatusbarItems({
}
}, [desktopActionTasks, previewServerRestartStatus, subagentsBySession, workingSessionIds])
const gatewayUp = Boolean(statusSnapshot?.gateway_running)
const gatewayOpen = gatewayState === 'open'
const inferenceReady = gatewayOpen && inferenceStatus?.ready === true
const gatewayDetail = gatewayOpen
? inferenceStatus
? inferenceReady
? 'ready'
: 'needs setup'
: 'checking'
: gatewayState === 'connecting'
? 'connecting'
: 'offline'
const gatewayClassName = inferenceReady
? undefined
: gatewayOpen || gatewayState === 'connecting'
? 'text-amber-600 hover:text-amber-600'
: 'text-destructive hover:text-destructive'
const versionItem = useMemo<StatusbarItem>(() => {
const appVersion = desktopVersion?.appVersion
@@ -182,14 +177,14 @@ export function useStatusbarItems({
variant: 'action'
},
{
className: gatewayUp ? undefined : 'text-destructive hover:text-destructive',
detail: gatewayUp ? statusSnapshot?.gateway_state || 'online' : 'offline',
icon: gatewayUp ? <Activity className="size-3" /> : <AlertCircle className="size-3" />,
className: gatewayClassName,
detail: gatewayDetail,
icon: inferenceReady ? <Activity className="size-3" /> : <AlertCircle className="size-3" />,
id: 'gateway-health',
label: 'Gateway',
menuClassName: 'w-72',
menuContent: gatewayMenuContent,
title: 'Gateway and platform health',
title: inferenceStatus?.reason || 'Hermes inference gateway status',
variant: 'menu'
},
{
@@ -234,9 +229,11 @@ export function useStatusbarItems({
bgRunning,
commandCenterOpen,
gatewayMenuContent,
gatewayUp,
gatewayClassName,
gatewayDetail,
inferenceReady,
inferenceStatus?.reason,
openAgents,
statusSnapshot?.gateway_state,
subagentsRunning,
toggleCommandCenter
]