From 553cf4f97757984965d4532a74cf17afdbd903b8 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 19 Jun 2026 10:02:54 -0500 Subject: [PATCH] feat(desktop): restart the gateway from Cmd+K, with statusbar spinner feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../desktop/src/app/command-palette/index.tsx | 9 ++++ .../app/shell/hooks/use-statusbar-items.tsx | 16 +++++-- apps/desktop/src/i18n/en.ts | 2 + apps/desktop/src/i18n/ja.ts | 2 + apps/desktop/src/i18n/types.ts | 2 + apps/desktop/src/i18n/zh-hant.ts | 2 + apps/desktop/src/i18n/zh.ts | 2 + apps/desktop/src/store/system-actions.ts | 48 +++++++++++++++++++ 8 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/store/system-actions.ts diff --git a/apps/desktop/src/app/command-palette/index.tsx b/apps/desktop/src/app/command-palette/index.tsx index 19ea7976344..54edc55fd54 100644 --- a/apps/desktop/src/app/command-palette/index.tsx +++ b/apps/desktop/src/app/command-palette/index.tsx @@ -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() } ] }, diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index b9a2d715454..a95ac3217f5 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -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 ? : , + className: gatewayRestarting ? undefined : gatewayClassName, + detail: gatewayRestarting ? copy.gatewayRestarting : gatewayDetail, + icon: gatewayRestarting ? ( + + ) : inferenceReady ? ( + + ) : ( + + ), id: 'gateway-health', label: copy.gateway, menuClassName: 'w-72', @@ -354,6 +363,7 @@ export function useStatusbarItems({ gatewayMenuContent, gatewayClassName, gatewayDetail, + gatewayRestarting, inferenceReady, inferenceStatus?.reason, openAgents, diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index d9876ccc1cd..7d2f54a5bfc 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -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', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 2fd12ad4281..467732dc992 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -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: 'エージェントを閉じる', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index b0932c5b2e2..df90b2c2c2e 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -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 diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 1ba307da876..1ece58d86a6 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -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: '關閉代理', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 5c58899f2b9..30e3a69b247 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -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: '关闭代理', diff --git a/apps/desktop/src/store/system-actions.ts b/apps/desktop/src/store/system-actions.ts new file mode 100644 index 00000000000..43a8d9b770e --- /dev/null +++ b/apps/desktop/src/store/system-actions.ts @@ -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 { + 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 { + $gatewayRestarting.set(true) + + try { + await awaitAction(await restartGateway()) + } catch (err) { + notifyError(err, translateNow('commandCenter.gatewayRestartFailed')) + } finally { + $gatewayRestarting.set(false) + } +}