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;
};