mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-18 14:52:04 +00:00
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:
parent
d0c20708ce
commit
c30550c552
12 changed files with 188 additions and 95 deletions
|
|
@ -352,6 +352,44 @@ function rememberLog(chunk) {
|
|||
scheduleDesktopLogFlush()
|
||||
}
|
||||
|
||||
function openExternalUrl(rawUrl) {
|
||||
const raw = String(rawUrl || '').trim()
|
||||
if (!raw) return false
|
||||
|
||||
let parsed
|
||||
try {
|
||||
parsed = new URL(raw)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const url = parsed.toString()
|
||||
|
||||
if (IS_WSL) {
|
||||
rememberLog(`[link] opening via WSL→Windows: ${url}`)
|
||||
const proc = spawn('cmd.exe', ['/c', 'start', '""', url], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
})
|
||||
proc.on('error', error => {
|
||||
rememberLog(`[link] cmd.exe start failed: ${error.message}; falling back to xdg-open`)
|
||||
shell.openExternal(url).catch(fallback => rememberLog(`[link] xdg-open failed: ${fallback.message}`))
|
||||
})
|
||||
proc.unref()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
shell.openExternal(url).catch(error => rememberLog(`[link] openExternal failed: ${error.message}`))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
|
@ -2067,7 +2105,7 @@ function installContextMenu(window) {
|
|||
label: 'Open Image',
|
||||
click: () => {
|
||||
if (params.srcURL && !params.srcURL.startsWith('data:')) {
|
||||
void shell.openExternal(params.srcURL)
|
||||
openExternalUrl(params.srcURL)
|
||||
}
|
||||
},
|
||||
enabled: !params.srcURL.startsWith('data:')
|
||||
|
|
@ -2096,7 +2134,7 @@ function installContextMenu(window) {
|
|||
template.push(
|
||||
{
|
||||
label: 'Open Link',
|
||||
click: () => void shell.openExternal(params.linkURL)
|
||||
click: () => openExternalUrl(params.linkURL)
|
||||
},
|
||||
{
|
||||
label: 'Copy Link',
|
||||
|
|
@ -2570,6 +2608,19 @@ function createWindow() {
|
|||
installPreviewShortcut(mainWindow)
|
||||
installDevToolsShortcut(mainWindow)
|
||||
installContextMenu(mainWindow)
|
||||
mainWindow.webContents.setWindowOpenHandler(details => {
|
||||
openExternalUrl(details.url)
|
||||
|
||||
return { action: 'deny' }
|
||||
})
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
if ((DEV_SERVER && url.startsWith(DEV_SERVER)) || (!DEV_SERVER && url.startsWith('file:'))) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
openExternalUrl(url)
|
||||
})
|
||||
|
||||
if (DEV_SERVER) {
|
||||
mainWindow.loadURL(DEV_SERVER)
|
||||
|
|
@ -2731,7 +2782,11 @@ ipcMain.on('hermes:titlebar-theme', (_event, payload) => {
|
|||
mainWindow?.setTitleBarOverlay?.(getTitleBarOverlayOptions())
|
||||
})
|
||||
|
||||
ipcMain.handle('hermes:openExternal', (_event, url) => shell.openExternal(url))
|
||||
ipcMain.handle('hermes:openExternal', (_event, url) => {
|
||||
if (!openExternalUrl(url)) {
|
||||
throw new Error('Invalid external URL')
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('hermes:fetchLinkTitle', (_event, url) => fetchLinkTitle(url))
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
"scripts": {
|
||||
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
|
||||
"dev:renderer": "vite --host 127.0.0.1 --port 5174",
|
||||
"dev:electron": "wait-on http://127.0.0.1:5174 && cross-env HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"profile:main": "wait-on http://127.0.0.1:5174 && cross-env HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "wait-on http://127.0.0.1:5174 && cross-env NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"dev:renderer": "node scripts/assert-root-install.cjs && vite --host 127.0.0.1 --port 5174",
|
||||
"dev:electron": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"profile:main": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"start": "npm run build && electron .",
|
||||
"build": "tsc -b && vite build",
|
||||
"build": "node scripts/assert-root-install.cjs && tsc -b && vite build",
|
||||
"stage:hermes": "node scripts/stage-hermes-payload.mjs",
|
||||
"builder": "cross-env NODE_OPTIONS=--max-old-space-size=16384 electron-builder",
|
||||
"pack": "npm run build && npm run stage:hermes && npm run builder -- --dir",
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'electron/**/*.{js,cjs}' 'vite.config.ts'",
|
||||
"fix": "npm run lint:fix && npm run fmt",
|
||||
"test:ui": "vitest run --environment jsdom",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4174"
|
||||
"preview": "node scripts/assert-root-install.cjs && vite preview --host 127.0.0.1 --port 4174"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistant-ui/react": "^0.12.28",
|
||||
|
|
|
|||
13
apps/desktop/scripts/assert-root-install.cjs
Normal file
13
apps/desktop/scripts/assert-root-install.cjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"use strict"
|
||||
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
const root = path.resolve(__dirname, "..", "..", "..")
|
||||
|
||||
try {
|
||||
fs.accessSync(path.join(root, "node_modules", "vite", "package.json"))
|
||||
} catch {
|
||||
console.error(`Run from repo root: cd ${root} && npm ci`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
@ -867,7 +867,7 @@ export function CommandCenterView({
|
|||
)}
|
||||
/>
|
||||
<span className="font-medium text-foreground">
|
||||
{status.gateway_running ? 'Gateway running' : 'Gateway not running'}
|
||||
{status.gateway_running ? 'Messaging gateway running' : 'Messaging gateway stopped'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
|
|
@ -876,7 +876,7 @@ export function CommandCenterView({
|
|||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5 whitespace-nowrap">
|
||||
<OverlayActionButton className="h-7 px-2.5" onClick={() => void runSystemAction('restart')}>
|
||||
Restart gateway
|
||||
Restart messaging
|
||||
</OverlayActionButton>
|
||||
<OverlayActionButton className="h-7 px-2.5" onClick={() => void runSystemAction('update')}>
|
||||
Update Hermes
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export function DesktopController() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const { gatewayLogLines, statusSnapshot } = useStatusSnapshot(gatewayState)
|
||||
const { gatewayLogLines, inferenceStatus, statusSnapshot } = useStatusSnapshot(gatewayState, requestGateway)
|
||||
|
||||
const { browseSessionCwd, changeSessionCwd, refreshProjectBranch } = useCwdActions({
|
||||
activeSessionId,
|
||||
|
|
@ -393,6 +393,8 @@ export function DesktopController() {
|
|||
extraLeftItems: statusbarItemGroups.flat.left,
|
||||
extraRightItems: statusbarItemGroups.flat.right,
|
||||
gatewayLogLines,
|
||||
gatewayState,
|
||||
inferenceStatus,
|
||||
openAgents,
|
||||
openCommandCenterSection,
|
||||
statusSnapshot,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const STATE_LABELS: Record<string, string> = {
|
|||
connecting: 'Connecting',
|
||||
disabled: 'Disabled',
|
||||
fatal: 'Error',
|
||||
gateway_stopped: 'Gateway stopped',
|
||||
gateway_stopped: 'Messaging gateway stopped',
|
||||
not_configured: 'Needs setup',
|
||||
pending_restart: 'Restart needed',
|
||||
retrying: 'Retrying',
|
||||
|
|
@ -502,7 +502,7 @@ function PlatformDetail({
|
|||
<SetupPill active={platform.configured}>
|
||||
{platform.configured ? 'Credentials set' : 'Needs setup'}
|
||||
</SetupPill>
|
||||
{!platform.gateway_running && <SetupPill active={false}>Gateway stopped</SetupPill>}
|
||||
{!platform.gateway_running && <SetupPill active={false}>Messaging gateway stopped</SetupPill>}
|
||||
</div>
|
||||
<PlatformHint platform={platform} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -53,9 +53,8 @@ function CodeHeader({ language, code }: { language?: string; code?: string }) {
|
|||
const label = cleanLanguage && cleanLanguage !== 'unknown' ? cleanLanguage : ''
|
||||
|
||||
return (
|
||||
<div className="m-0 flex items-stretch justify-between gap-2 rounded-t-md border border-b-0 border-border bg-muted/60 pr-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-2.5 py-1.5 pl-0 font-mono uppercase tracking-[0.16em]">
|
||||
<span aria-hidden="true" className="self-stretch w-[2px] -my-1.5 bg-midground/60" />
|
||||
<div className="aui-code-header m-0 flex items-stretch justify-between gap-2 rounded-t-md border border-b-0 border-border bg-muted/60 pr-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-2.5 py-1.5 pl-3 font-mono uppercase tracking-[0.16em]">
|
||||
<span className="text-midground/85">{label || 'code'}</span>
|
||||
</span>
|
||||
<CopyButton appearance="inline" iconClassName="size-3" label="Copy code" text={normalizedCode}>
|
||||
|
|
@ -288,10 +287,10 @@ const MarkdownTextImpl = () => {
|
|||
<li className={cn('leading-(--dt-line-height)', className)} {...props} />
|
||||
),
|
||||
table: ({ className, ...props }: ComponentProps<'table'>) => (
|
||||
<div className="max-w-full overflow-x-auto rounded-md border border-border">
|
||||
<div className="aui-md-table my-3 max-w-full overflow-x-auto rounded-md border border-border">
|
||||
<table
|
||||
className={cn(
|
||||
'w-full border-collapse text-sm [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
||||
'm-0 w-full border-collapse text-sm [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -299,12 +298,12 @@ const MarkdownTextImpl = () => {
|
|||
</div>
|
||||
),
|
||||
thead: ({ className, ...props }: ComponentProps<'thead'>) => (
|
||||
<thead className={cn('bg-muted/50 text-foreground', className)} {...props} />
|
||||
<thead className={cn('m-0 bg-muted/50 text-foreground', className)} {...props} />
|
||||
),
|
||||
th: ({ className, ...props }: ComponentProps<'th'>) => (
|
||||
<th
|
||||
className={cn(
|
||||
'h-9 px-3 text-left align-middle text-xs font-semibold uppercase tracking-[0.16em] text-midground/75',
|
||||
'px-3 py-1.5 text-left align-middle text-xs font-semibold uppercase tracking-[0.16em] text-midground/75',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ export const SyntaxHighlighter: FC<HermesSyntaxHighlighterProps> = ({
|
|||
code,
|
||||
defer = false
|
||||
}) => {
|
||||
const preClassName =
|
||||
'aui-shiki m-0 overflow-hidden rounded-b-md border border-t-0 border-border bg-card font-mono text-sm leading-relaxed [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-transparent! [&_pre]:px-4 [&_pre]:py-3 [&_pre]:font-mono [&_pre]:leading-relaxed'
|
||||
|
||||
// Streamdown may hand us fence contents with edge newlines. Strip blank
|
||||
// fence padding without touching indentation on the first real line.
|
||||
const trimmed = (code ?? '').replace(/^\n+/, '').trimEnd()
|
||||
|
|
@ -45,14 +48,14 @@ export const SyntaxHighlighter: FC<HermesSyntaxHighlighterProps> = ({
|
|||
|
||||
if (defer) {
|
||||
return (
|
||||
<Pre className="aui-shiki m-0 overflow-hidden rounded-b-md border border-t-0 border-border bg-card font-mono text-sm leading-relaxed [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-transparent! [&_pre]:px-4 [&_pre]:py-3 [&_pre]:font-mono [&_pre]:leading-relaxed">
|
||||
<Pre className={preClassName}>
|
||||
<code className="block whitespace-pre">{trimmed}</code>
|
||||
</Pre>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Pre className="aui-shiki m-0 overflow-hidden rounded-b-md border border-t-0 border-border bg-card font-mono text-sm leading-relaxed [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-transparent! [&_pre]:px-4 [&_pre]:py-3 [&_pre]:font-mono [&_pre]:leading-relaxed">
|
||||
<Pre className={preClassName}>
|
||||
<ShikiHighlighter
|
||||
addDefaultStyles={false}
|
||||
as="div"
|
||||
|
|
|
|||
|
|
@ -457,17 +457,25 @@ canvas {
|
|||
white-space: inherit;
|
||||
}
|
||||
|
||||
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] {
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
margin: 1rem 0 !important;
|
||||
[data-slot='aui_assistant-message-content'] .aui-md :where(.aui-code-header, .aui-shiki, .aui-shiki > pre) {
|
||||
margin: 0 !important;
|
||||
margin-block-start: 0 !important;
|
||||
margin-block-end: 0 !important;
|
||||
}
|
||||
|
||||
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] > * {
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table > table,
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table thead,
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table tbody,
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table tr,
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table th,
|
||||
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table td {
|
||||
margin: 0 !important;
|
||||
margin-block-start: 0 !important;
|
||||
margin-block-end: 0 !important;
|
||||
}
|
||||
|
||||
[data-slot='tool-block'] + [data-slot='tool-block'] {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue