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)
+ }
+}