mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
fix(desktop): rename "Restart messaging" → "Restart gateway", surface restarts in the statusbar, make logs selectable (#49094)
* fix(desktop): rename "Restart messaging" -> "Restart gateway"
The Command Center control restarts the whole messaging gateway, yet was
labelled "Restart messaging" while the status line above it reads "Messaging
gateway running/stopped". Rename the i18n key to match what it does, across
all 4 locales.
* 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.
* fix(desktop): offer a Restart gateway action on messaging save/toggle toasts
The "setup saved" and "platform enabled/disabled" toasts told users their
change needs a gateway restart but left it a separate hunt. Attach a "Restart
gateway" action (the shared runGatewayRestart), and reword the copy to state
the pending consequence ("...takes effect after a gateway restart") now that
the button carries the verb. Updated all 4 locales.
* fix(desktop): make rendered logs selectable so they can be copied
The global body { user-select: none } left log surfaces unselectable. Opt them
back in via the existing data-selectable-text convention — at the shared
LogView primitive (boot-failure + bootstrap install overlays) plus Command
Center recent logs, toolset post-setup output, notification detail, and
subagent stream/file lines.
This commit is contained in:
commit
0e8b76532e
14 changed files with 117 additions and 24 deletions
|
|
@ -357,7 +357,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n
|
|||
</button>
|
||||
|
||||
{visibleRows.length > 0 ? (
|
||||
<div className="grid min-w-0 gap-1 pl-6">
|
||||
<div className="grid min-w-0 gap-1 pl-6" data-selectable-text="true">
|
||||
{visibleRows.map((entry, i) => (
|
||||
<StreamLine
|
||||
active={running && i === visibleRows.length - 1}
|
||||
|
|
@ -371,7 +371,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n
|
|||
) : null}
|
||||
|
||||
{open && fileLines.length > 0 ? (
|
||||
<div className="grid min-w-0 gap-0.5 pl-6">
|
||||
<div className="grid min-w-0 gap-0.5 pl-6" data-selectable-text="true">
|
||||
<p className="text-[0.58rem] font-medium tracking-wider text-muted-foreground/60 uppercase">
|
||||
{t.agents.files}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ export function CommandCenterView({ initialSection, onClose, onDeleteSession, on
|
|||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5 whitespace-nowrap">
|
||||
<Button onClick={() => void runSystemAction('restart')} size="xs" variant="text">
|
||||
{cc.restartMessaging}
|
||||
{cc.restartGateway}
|
||||
</Button>
|
||||
<Button onClick={() => void runSystemAction('update')} size="xs" variant="textStrong">
|
||||
{cc.updateHermes}
|
||||
|
|
@ -426,7 +426,10 @@ export function CommandCenterView({ initialSection, onClose, onDeleteSession, on
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<pre className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap wrap-break-word rounded-lg border border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) p-3 font-mono text-[0.65rem] leading-relaxed text-(--ui-text-tertiary)">
|
||||
<pre
|
||||
className="min-h-0 flex-1 overflow-auto whitespace-pre-wrap wrap-break-word rounded-lg border border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) p-3 font-mono text-[0.65rem] leading-relaxed text-(--ui-text-tertiary)"
|
||||
data-selectable-text="true"
|
||||
>
|
||||
{logs.length ? logs.join('\n') : cc.noLogs}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<MessagingPlatformInfo[] | null>(null)
|
||||
const [edits, setEdits] = useState<EditMap>({})
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -272,7 +272,10 @@ function PostSetupRunner({ toolset, postSetupKey, onComplete }: PostSetupRunnerP
|
|||
</div>
|
||||
|
||||
{status && (status.lines.length > 0 || status.running) && (
|
||||
<pre className="max-h-48 overflow-y-auto rounded-md bg-background px-2.5 py-1.5 font-mono text-[0.7rem] leading-relaxed text-muted-foreground whitespace-pre-wrap">
|
||||
<pre
|
||||
className="max-h-48 overflow-y-auto rounded-md bg-background px-2.5 py-1.5 font-mono text-[0.7rem] leading-relaxed text-muted-foreground whitespace-pre-wrap"
|
||||
data-selectable-text="true"
|
||||
>
|
||||
{status.lines.length > 0 ? status.lines.join('\n') : copy.postSetupStarting}
|
||||
</pre>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,10 @@ function NotificationDetail({ detail }: { detail: string }) {
|
|||
<details className="mt-2 text-xs text-muted-foreground">
|
||||
<summary className="select-none font-medium text-muted-foreground hover:text-foreground">{copy.details}</summary>
|
||||
<div className="mt-1 rounded-md bg-background/65 p-2">
|
||||
<pre className="max-h-32 whitespace-pre-wrap wrap-break-word font-mono text-[0.6875rem] leading-relaxed">
|
||||
<pre
|
||||
className="max-h-32 whitespace-pre-wrap wrap-break-word font-mono text-[0.6875rem] leading-relaxed"
|
||||
data-selectable-text="true"
|
||||
>
|
||||
{detail}
|
||||
</pre>
|
||||
<CopyButton
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
|
|||
|
||||
// Shared raw-log viewer: no bg, hairline border, tight padding, small mono.
|
||||
// One style everywhere we surface logs. Pass a max-h-* via className.
|
||||
// Selectable by default — logs exist to be read and copied.
|
||||
export function LogView({ className, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -11,6 +12,7 @@ export function LogView({ className, ...props }: ComponentProps<'div'>) {
|
|||
'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}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'エージェントを閉じる',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '關閉代理',
|
||||
|
|
|
|||
|
|
@ -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: '关闭代理',
|
||||
|
|
|
|||
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