diff --git a/apps/desktop/src/app/agents/index.tsx b/apps/desktop/src/app/agents/index.tsx index ec8f186dd1b..6a1fbf9eeea 100644 --- a/apps/desktop/src/app/agents/index.tsx +++ b/apps/desktop/src/app/agents/index.tsx @@ -357,7 +357,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n {visibleRows.length > 0 ? ( -
+
{visibleRows.map((entry, i) => ( 0 ? ( -
+

{t.agents.files}

diff --git a/apps/desktop/src/app/command-center/index.tsx b/apps/desktop/src/app/command-center/index.tsx index 137b4e6e049..57358186a03 100644 --- a/apps/desktop/src/app/command-center/index.tsx +++ b/apps/desktop/src/app/command-center/index.tsx @@ -395,7 +395,7 @@ export function CommandCenterView({ initialSection, onClose, onDeleteSession, on
-
+                
                   {logs.length ? logs.join('\n') : cc.noLogs}
                 
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/messaging/index.tsx b/apps/desktop/src/app/messaging/index.tsx index 7fc6ce212ef..f7f3eaa91e2 100644 --- a/apps/desktop/src/app/messaging/index.tsx +++ b/apps/desktop/src/app/messaging/index.tsx @@ -17,6 +17,7 @@ import { type Translations, useI18n } from '@/i18n' import { AlertTriangle, ExternalLink, Save, Trash2 } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' +import { runGatewayRestart } from '@/store/system-actions' import { useRefreshHotkey } from '../hooks/use-refresh-hotkey' import { useRouteEnumParam } from '../hooks/use-route-enum-param' @@ -97,6 +98,8 @@ function fieldCopy(field: MessagingEnvVarInfo, m: Translations['messaging']) { export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: MessagingViewProps) { const { t } = useI18n() const m = t.messaging + // Both save/toggle toasts offer the same one-click restart. + const restartGatewayAction = { label: t.commandCenter.restartGateway, onClick: () => void runGatewayRestart() } const [platforms, setPlatforms] = useState(null) const [edits, setEdits] = useState({}) const [query, setQuery] = useState('') @@ -197,7 +200,8 @@ export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, . notify({ kind: 'success', title: enabled ? m.platformEnabled(platform.name) : m.platformDisabled(platform.name), - message: m.restartToApply + message: m.restartToApply, + action: restartGatewayAction }) } catch (err) { notifyError(err, m.failedUpdate(platform.name)) @@ -222,7 +226,8 @@ export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, . notify({ kind: 'success', title: m.setupSaved(platform.name), - message: m.restartToReconnect + message: m.restartToReconnect, + action: restartGatewayAction }) } catch (err) { notifyError(err, m.failedSave(platform.name)) diff --git a/apps/desktop/src/app/settings/toolset-config-panel.tsx b/apps/desktop/src/app/settings/toolset-config-panel.tsx index a321096f183..d98ff2a9ace 100644 --- a/apps/desktop/src/app/settings/toolset-config-panel.tsx +++ b/apps/desktop/src/app/settings/toolset-config-panel.tsx @@ -272,7 +272,10 @@ function PostSetupRunner({ toolset, postSetupKey, onComplete }: PostSetupRunnerP
{status && (status.lines.length > 0 || status.running) && ( -
+        
           {status.lines.length > 0 ? status.lines.join('\n') : copy.postSetupStarting}
         
)} 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/components/notifications.tsx b/apps/desktop/src/components/notifications.tsx index ed26edbec0a..2558d27f93f 100644 --- a/apps/desktop/src/components/notifications.tsx +++ b/apps/desktop/src/components/notifications.tsx @@ -154,7 +154,10 @@ function NotificationDetail({ detail }: { detail: string }) {
{copy.details}
-
+        
           {detail}
         
) { return (
) { 'overflow-auto rounded-lg border border-(--ui-stroke-tertiary) px-2.5 py-1.5 font-mono text-[0.6875rem] leading-[1.5] whitespace-pre-wrap break-words text-(--ui-text-tertiary) [scrollbar-width:thin]', className )} + data-selectable-text="true" {...props} /> ) diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 158de543c49..afe1e0117a2 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -763,7 +763,8 @@ export const en: Translations = { gatewayRunning: 'Messaging gateway running', gatewayStopped: 'Messaging gateway stopped', hermesActiveSessions: (version, count) => `Hermes ${version} · Active sessions ${count}`, - restartMessaging: 'Restart messaging', + restartGateway: 'Restart gateway', + gatewayRestartFailed: 'Gateway restart failed.', updateHermes: 'Update Hermes', actionRunning: 'running', actionDone: 'done', @@ -832,9 +833,9 @@ export const en: Translations = { disableAria: name => `Disable ${name}`, platformEnabled: name => `${name} enabled`, platformDisabled: name => `${name} disabled`, - restartToApply: 'Restart the gateway for this change to take effect.', + restartToApply: 'This change takes effect after a gateway restart.', setupSaved: name => `${name} setup saved`, - restartToReconnect: 'Restart the gateway to reconnect with the new credentials.', + restartToReconnect: 'New credentials take effect after a gateway restart.', keyCleared: key => `${key} cleared`, setupUpdated: name => `${name} setup was updated.`, failedUpdate: name => `Failed to update ${name}`, @@ -1589,6 +1590,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 244fc12ca49..03fd9b4354b 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -883,7 +883,8 @@ export const ja = defineLocale({ gatewayRunning: 'メッセージングゲートウェイが実行中', gatewayStopped: 'メッセージングゲートウェイが停止中', hermesActiveSessions: (version, count) => `Hermes ${version} · アクティブセッション ${count}`, - restartMessaging: 'メッセージングを再起動', + restartGateway: 'ゲートウェイを再起動', + gatewayRestartFailed: 'ゲートウェイの再起動に失敗しました。', updateHermes: 'Hermes を更新', actionRunning: '実行中', actionDone: '完了', @@ -953,9 +954,9 @@ export const ja = defineLocale({ disableAria: name => `${name} を無効にする`, platformEnabled: name => `${name} を有効にしました`, platformDisabled: name => `${name} を無効にしました`, - restartToApply: 'この変更を有効にするにはゲートウェイを再起動してください。', + restartToApply: 'この変更はゲートウェイの再起動後に有効になります。', setupSaved: name => `${name} の設定を保存しました`, - restartToReconnect: '新しい認証情報で再接続するにはゲートウェイを再起動してください。', + restartToReconnect: '新しい認証情報はゲートウェイの再起動後に有効になります。', keyCleared: key => `${key} をクリアしました`, setupUpdated: name => `${name} の設定が更新されました。`, failedUpdate: name => `${name} の更新に失敗しました`, @@ -1719,6 +1720,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 90168d28e86..da025767fff 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -627,7 +627,8 @@ export interface Translations { gatewayRunning: string gatewayStopped: string hermesActiveSessions: (version: string, count: number) => string - restartMessaging: string + restartGateway: string + gatewayRestartFailed: string updateHermes: string actionRunning: string actionDone: string @@ -1231,6 +1232,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 c1eb3b8f883..b60fe5d423d 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -856,7 +856,8 @@ export const zhHant = defineLocale({ gatewayRunning: '訊息閘道執行中', gatewayStopped: '訊息閘道已停止', hermesActiveSessions: (version, count) => `Hermes ${version} · 活躍工作階段 ${count}`, - restartMessaging: '重新啟動訊息服務', + restartGateway: '重新啟動閘道', + gatewayRestartFailed: '閘道重新啟動失敗。', updateHermes: '更新 Hermes', actionRunning: '執行中', actionDone: '完成', @@ -925,9 +926,9 @@ export const zhHant = defineLocale({ disableAria: name => `停用 ${name}`, platformEnabled: name => `${name} 已啟用`, platformDisabled: name => `${name} 已停用`, - restartToApply: '重新啟動閘道後此變更才會生效。', + restartToApply: '此變更將在閘道重新啟動後生效。', setupSaved: name => `${name} 設定已儲存`, - restartToReconnect: '重新啟動閘道以使用新憑證重新連線。', + restartToReconnect: '新憑證將在閘道重新啟動後生效。', keyCleared: key => `${key} 已清除`, setupUpdated: name => `${name} 設定已更新。`, failedUpdate: name => `更新 ${name} 失敗`, @@ -1663,6 +1664,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 161a438b9e7..bc0b828b955 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -953,7 +953,8 @@ export const zh: Translations = { gatewayRunning: '消息网关运行中', gatewayStopped: '消息网关已停止', hermesActiveSessions: (version, count) => `Hermes ${version} · 活跃会话 ${count}`, - restartMessaging: '重启消息服务', + restartGateway: '重启网关', + gatewayRestartFailed: '网关重启失败。', updateHermes: '更新 Hermes', actionRunning: '运行中', actionDone: '完成', @@ -1022,9 +1023,9 @@ export const zh: Translations = { disableAria: name => `禁用 ${name}`, platformEnabled: name => `${name} 已启用`, platformDisabled: name => `${name} 已禁用`, - restartToApply: '重启网关后此更改才会生效。', + restartToApply: '此更改将在网关重启后生效。', setupSaved: name => `${name} 设置已保存`, - restartToReconnect: '重启网关以使用新凭据重新连接。', + restartToReconnect: '新凭据将在网关重启后生效。', keyCleared: key => `${key} 已清除`, setupUpdated: name => `${name} 设置已更新。`, failedUpdate: name => `更新 ${name} 失败`, @@ -1769,6 +1770,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) + } +}