diff --git a/web/src/App.tsx b/web/src/App.tsx index ba7b5af8d4f..79f7e485350 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -100,7 +100,7 @@ import type { PluginManifest } from "@/plugins"; import { useTheme } from "@/themes"; import { isDashboardEmbeddedChatEnabled } from "@/lib/dashboard-flags"; import { api } from "@/lib/api"; -import type { StatusResponse } from "@/lib/api"; +import type { StatusResponse, UpdateCheckResponse } from "@/lib/api"; function RootRedirect() { return ; @@ -903,6 +903,46 @@ function SidebarSystemActions({ useSystemActions(); const canUpdateHermes = status?.can_update_hermes === true; const [restartConfirmOpen, setRestartConfirmOpen] = useState(false); + const [updateConfirmOpen, setUpdateConfirmOpen] = useState(false); + const [updateConfirmInfo, setUpdateConfirmInfo] = + useState(null); + const [updateConfirmChecking, setUpdateConfirmChecking] = useState(false); + + useEffect(() => { + if (!updateConfirmOpen) { + setUpdateConfirmInfo(null); + return; + } + let cancelled = false; + setUpdateConfirmChecking(true); + api + .checkHermesUpdate(false) + .then((info) => { + if (!cancelled) setUpdateConfirmInfo(info); + }) + .catch(() => { + if (!cancelled) setUpdateConfirmInfo(null); + }) + .finally(() => { + if (!cancelled) setUpdateConfirmChecking(false); + }); + return () => { + cancelled = true; + }; + }, [updateConfirmOpen]); + + const updateConfirmDescription = useMemo(() => { + if (updateConfirmInfo?.behind && updateConfirmInfo.behind > 0) { + const cmd = updateConfirmInfo.update_command; + const n = updateConfirmInfo.behind; + return `This will run 'hermes update' (${cmd}) and pull ${n} new commit${n === 1 ? "" : "s"}. The gateway restarts when the update finishes; the current session keeps its prompt cache until then.`; + } + const cmd = updateConfirmInfo?.update_command ?? "hermes update"; + return ( + t.status.updateHermesConfirmMessage ?? + `This will run 'hermes update' (${cmd}) and restart the gateway when it finishes.` + ); + }, [t.status.updateHermesConfirmMessage, updateConfirmInfo]); const items: SystemActionItem[] = [ { @@ -929,6 +969,10 @@ function SidebarSystemActions({ setRestartConfirmOpen(true); return; } + if (action === "update") { + setUpdateConfirmOpen(true); + return; + } void runAction(action); navigate("/sessions"); onNavigate(); @@ -941,6 +985,13 @@ function SidebarSystemActions({ onNavigate(); }; + const confirmUpdate = () => { + setUpdateConfirmOpen(false); + void runAction("update"); + navigate("/sessions"); + onNavigate(); + }; + return ( <>
+ + setUpdateConfirmOpen(false)} + onConfirm={confirmUpdate} + open={updateConfirmOpen} + title={t.status.updateHermesConfirmTitle ?? `${t.status.updateHermes}?`} + /> ); } diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 9768bf2d369..e2ba0cc0369 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -132,6 +132,10 @@ export const en: Translations = { startedInBackground: "Started in background — check logs for progress", stopped: "Stopped", updateHermes: "Update Hermes", + updateHermesConfirmMessage: + "This runs hermes update and restarts the gateway when it finishes. Active sessions keep their prompt cache until then.", + updateHermesConfirmNow: "Update now", + updateHermesConfirmTitle: "Update Hermes?", updatingHermes: "Updating Hermes…", waitingForOutput: "Waiting for output…", }, diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index 77f1969aede..a93f921cadf 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -149,6 +149,9 @@ export interface Translations { startedInBackground: string; stopped: string; updateHermes: string; + updateHermesConfirmMessage?: string; + updateHermesConfirmNow?: string; + updateHermesConfirmTitle?: string; updatingHermes: string; waitingForOutput: string; };