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:
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user