mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
feat(desktop): restart the gateway from Cmd+K, with statusbar spinner feedback
Add a shared runGatewayRestart() (store/system-actions.ts) and wire it to a new Cmd+K "Restart gateway" action. While a restart is in flight the statusbar "Gateway" item swaps its icon for the TUI glyph spinner and reads "restarting…", returning to its real state on completion — driven by a $gatewayRestarting atom, not a transient toast or the generic "Agents running" counter. The helper owns its error handling so fire-and-forget callers can't leak an unhandled rejection; only a failure toasts.
This commit is contained in:
parent
6308d3416a
commit
553cf4f977
8 changed files with 80 additions and 3 deletions
|
|
@ -30,6 +30,7 @@ import {
|
|||
Package,
|
||||
Palette,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Settings2,
|
||||
Sun,
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
import { cn } from '@/lib/utils'
|
||||
import { $commandPaletteOpen, closeCommandPalette, setCommandPaletteOpen } from '@/store/command-palette'
|
||||
import { $bindings } from '@/store/keybinds'
|
||||
import { runGatewayRestart } from '@/store/system-actions'
|
||||
import { luminance } from '@/themes/color'
|
||||
import { type ThemeMode, useTheme } from '@/themes/context'
|
||||
import { isUserTheme, resolveTheme } from '@/themes/user-themes'
|
||||
|
|
@ -360,6 +362,13 @@ export function CommandPalette() {
|
|||
keywords: ['command center', 'usage', 'tokens', 'cost'],
|
||||
label: cc.sections.usage,
|
||||
run: go(`${COMMAND_CENTER_ROUTE}?section=usage`)
|
||||
},
|
||||
{
|
||||
icon: RefreshCw,
|
||||
id: 'cc-restart-gateway',
|
||||
keywords: ['gateway', 'restart', 'messaging', 'reconnect', 'system'],
|
||||
label: cc.restartGateway,
|
||||
run: () => void runGatewayRestart()
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'
|
|||
import type { CommandCenterSection } from '@/app/command-center'
|
||||
import { $terminalTakeover, setTerminalTakeover } from '@/app/right-sidebar/store'
|
||||
import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel'
|
||||
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
|
||||
import { useI18n } from '@/i18n'
|
||||
import {
|
||||
Activity,
|
||||
|
|
@ -35,6 +36,7 @@ import {
|
|||
setYoloActive
|
||||
} from '@/store/session'
|
||||
import { $subagentsBySession, activeSubagentCount } from '@/store/subagents'
|
||||
import { $gatewayRestarting } from '@/store/system-actions'
|
||||
import {
|
||||
$backendUpdateApply,
|
||||
$backendUpdateStatus,
|
||||
|
|
@ -89,6 +91,7 @@ export function useStatusbarItems({
|
|||
const busy = useStore($busy)
|
||||
const currentUsage = useStore($currentUsage)
|
||||
const desktopActionTasks = useStore($desktopActionTasks)
|
||||
const gatewayRestarting = useStore($gatewayRestarting)
|
||||
const previewServerRestartStatus = useStore($previewServerRestartStatus)
|
||||
const sessionStartedAt = useStore($sessionStartedAt)
|
||||
const turnStartedAt = useStore($turnStartedAt)
|
||||
|
|
@ -299,9 +302,15 @@ export function useStatusbarItems({
|
|||
variant: 'action'
|
||||
},
|
||||
{
|
||||
className: gatewayClassName,
|
||||
detail: gatewayDetail,
|
||||
icon: inferenceReady ? <Activity className="size-3" /> : <AlertCircle className="size-3" />,
|
||||
className: gatewayRestarting ? undefined : gatewayClassName,
|
||||
detail: gatewayRestarting ? copy.gatewayRestarting : gatewayDetail,
|
||||
icon: gatewayRestarting ? (
|
||||
<GlyphSpinner ariaLabel={copy.gatewayRestarting} className="size-3" />
|
||||
) : inferenceReady ? (
|
||||
<Activity className="size-3" />
|
||||
) : (
|
||||
<AlertCircle className="size-3" />
|
||||
),
|
||||
id: 'gateway-health',
|
||||
label: copy.gateway,
|
||||
menuClassName: 'w-72',
|
||||
|
|
@ -354,6 +363,7 @@ export function useStatusbarItems({
|
|||
gatewayMenuContent,
|
||||
gatewayClassName,
|
||||
gatewayDetail,
|
||||
gatewayRestarting,
|
||||
inferenceReady,
|
||||
inferenceStatus?.reason,
|
||||
openAgents,
|
||||
|
|
|
|||
|
|
@ -762,6 +762,7 @@ export const en: Translations = {
|
|||
gatewayStopped: 'Messaging gateway stopped',
|
||||
hermesActiveSessions: (version, count) => `Hermes ${version} · Active sessions ${count}`,
|
||||
restartGateway: 'Restart gateway',
|
||||
gatewayRestartFailed: 'Gateway restart failed.',
|
||||
updateHermes: 'Update Hermes',
|
||||
actionRunning: 'running',
|
||||
actionDone: 'done',
|
||||
|
|
@ -1587,6 +1588,7 @@ export const en: Translations = {
|
|||
gatewayChecking: 'checking',
|
||||
gatewayConnecting: 'connecting',
|
||||
gatewayOffline: 'offline',
|
||||
gatewayRestarting: 'restarting…',
|
||||
gatewayTitle: 'Hermes inference gateway status',
|
||||
agents: 'Agents',
|
||||
closeAgents: 'Close agents',
|
||||
|
|
|
|||
|
|
@ -882,6 +882,7 @@ export const ja = defineLocale({
|
|||
gatewayStopped: 'メッセージングゲートウェイが停止中',
|
||||
hermesActiveSessions: (version, count) => `Hermes ${version} · アクティブセッション ${count}`,
|
||||
restartGateway: 'ゲートウェイを再起動',
|
||||
gatewayRestartFailed: 'ゲートウェイの再起動に失敗しました。',
|
||||
updateHermes: 'Hermes を更新',
|
||||
actionRunning: '実行中',
|
||||
actionDone: '完了',
|
||||
|
|
@ -1717,6 +1718,7 @@ export const ja = defineLocale({
|
|||
gatewayChecking: '確認中',
|
||||
gatewayConnecting: '接続中',
|
||||
gatewayOffline: 'オフライン',
|
||||
gatewayRestarting: '再起動中…',
|
||||
gatewayTitle: 'Hermes 推論ゲートウェイのステータス',
|
||||
agents: 'エージェント',
|
||||
closeAgents: 'エージェントを閉じる',
|
||||
|
|
|
|||
|
|
@ -626,6 +626,7 @@ export interface Translations {
|
|||
gatewayStopped: string
|
||||
hermesActiveSessions: (version: string, count: number) => string
|
||||
restartGateway: string
|
||||
gatewayRestartFailed: string
|
||||
updateHermes: string
|
||||
actionRunning: string
|
||||
actionDone: string
|
||||
|
|
@ -1229,6 +1230,7 @@ export interface Translations {
|
|||
gatewayChecking: string
|
||||
gatewayConnecting: string
|
||||
gatewayOffline: string
|
||||
gatewayRestarting: string
|
||||
gatewayTitle: string
|
||||
agents: string
|
||||
closeAgents: string
|
||||
|
|
|
|||
|
|
@ -855,6 +855,7 @@ export const zhHant = defineLocale({
|
|||
gatewayStopped: '訊息閘道已停止',
|
||||
hermesActiveSessions: (version, count) => `Hermes ${version} · 活躍工作階段 ${count}`,
|
||||
restartGateway: '重新啟動閘道',
|
||||
gatewayRestartFailed: '閘道重新啟動失敗。',
|
||||
updateHermes: '更新 Hermes',
|
||||
actionRunning: '執行中',
|
||||
actionDone: '完成',
|
||||
|
|
@ -1661,6 +1662,7 @@ export const zhHant = defineLocale({
|
|||
gatewayChecking: '檢查中',
|
||||
gatewayConnecting: '連線中',
|
||||
gatewayOffline: '離線',
|
||||
gatewayRestarting: '重新啟動中…',
|
||||
gatewayTitle: 'Hermes 推論閘道狀態',
|
||||
agents: '代理',
|
||||
closeAgents: '關閉代理',
|
||||
|
|
|
|||
|
|
@ -952,6 +952,7 @@ export const zh: Translations = {
|
|||
gatewayStopped: '消息网关已停止',
|
||||
hermesActiveSessions: (version, count) => `Hermes ${version} · 活跃会话 ${count}`,
|
||||
restartGateway: '重启网关',
|
||||
gatewayRestartFailed: '网关重启失败。',
|
||||
updateHermes: '更新 Hermes',
|
||||
actionRunning: '运行中',
|
||||
actionDone: '完成',
|
||||
|
|
@ -1767,6 +1768,7 @@ export const zh: Translations = {
|
|||
gatewayChecking: '检查中',
|
||||
gatewayConnecting: '连接中',
|
||||
gatewayOffline: '离线',
|
||||
gatewayRestarting: '重启中…',
|
||||
gatewayTitle: 'Hermes 推理网关状态',
|
||||
agents: '代理',
|
||||
closeAgents: '关闭代理',
|
||||
|
|
|
|||
48
apps/desktop/src/store/system-actions.ts
Normal file
48
apps/desktop/src/store/system-actions.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { atom } from 'nanostores'
|
||||
|
||||
import { getActionStatus, restartGateway } from '@/hermes'
|
||||
import { translateNow } from '@/i18n'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import type { ActionResponse } from '@/types/hermes'
|
||||
|
||||
const POLL_ATTEMPTS = 18
|
||||
const POLL_INTERVAL_MS = 1200
|
||||
const POLL_TIMEOUT_S = 180
|
||||
|
||||
// True while a gateway restart is in flight — drives the statusbar gateway
|
||||
// indicator (glyph spinner) so the restart shows up where users already look,
|
||||
// instead of a toast that vanishes or a generic "Agents running" counter.
|
||||
export const $gatewayRestarting = atom(false)
|
||||
|
||||
// Poll a backend action to completion (or a bounded window), throwing on a
|
||||
// non-zero exit so the caller can surface the failure.
|
||||
async function awaitAction(started: ActionResponse): Promise<void> {
|
||||
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
|
||||
await new Promise(resolve => window.setTimeout(resolve, POLL_INTERVAL_MS))
|
||||
const status = await getActionStatus(started.name, POLL_TIMEOUT_S)
|
||||
|
||||
if (!status.running) {
|
||||
if (status.exit_code != null && status.exit_code !== 0) {
|
||||
throw new Error(translateNow('commandCenter.gatewayRestartFailed'))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restart the messaging gateway, surfacing progress in the statusbar gateway
|
||||
// indicator. Self-contained and never rejects, so every trigger — Cmd+K, the
|
||||
// messaging save/toggle toasts — gets identical feedback from a plain
|
||||
// `void runGatewayRestart()`, and a failure is the only thing that toasts.
|
||||
export async function runGatewayRestart(): Promise<void> {
|
||||
$gatewayRestarting.set(true)
|
||||
|
||||
try {
|
||||
await awaitAction(await restartGateway())
|
||||
} catch (err) {
|
||||
notifyError(err, translateNow('commandCenter.gatewayRestartFailed'))
|
||||
} finally {
|
||||
$gatewayRestarting.set(false)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue