hermes-agent/web/src/contexts/SystemActions.tsx
Austin Pickett c9e5a9bb08 refactor(web): consume DS primitives, remove local component copies
Replace locally-forked UI components and hooks with their newly
promoted counterparts from @nous-research/ui:

Deleted local components (now in DS):
- components/ui/input.tsx, label.tsx, separator.tsx, card.tsx,
  confirm-dialog.tsx
- components/Toast.tsx, BottomPickSheet.tsx, NouiTypography.tsx
- hooks/useToast.ts, useModalBehavior.ts, useBelowBreakpoint.ts,
  useConfirmDelete.ts

Import updates across 25 files to use DS deep imports:
- @nous-research/ui/ui/components/{input,label,separator,card,
  confirm-dialog,toast,bottom-sheet}
- @nous-research/ui/ui/components/typography (replaces NouiTypography)
- @nous-research/ui/hooks/{use-toast,use-modal-behavior,
  use-below-breakpoint,use-confirm-delete}

Requires design-language >= feat/promote-hermes-web-primitives.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 21:57:59 -04:00

120 lines
3.1 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { api } from "@/lib/api";
import type { ActionStatusResponse } from "@/lib/api";
import { Toast } from "@nous-research/ui/ui/components/toast";
import { useI18n } from "@/i18n";
import {
SystemActionsContext,
type SystemAction,
} from "./system-actions-context";
const ACTION_NAMES: Record<SystemAction, string> = {
restart: "gateway-restart",
update: "hermes-update",
};
export function SystemActionsProvider({
children,
}: {
children: React.ReactNode;
}) {
const [pendingAction, setPendingAction] = useState<SystemAction | null>(null);
const [activeAction, setActiveAction] = useState<SystemAction | null>(null);
const [actionStatus, setActionStatus] = useState<ActionStatusResponse | null>(
null,
);
const [toast, setToast] = useState<ToastState | null>(null);
const { t } = useI18n();
useEffect(() => {
if (!toast) return;
const timer = setTimeout(() => setToast(null), 4000);
return () => clearTimeout(timer);
}, [toast]);
useEffect(() => {
if (!activeAction) return;
const name = ACTION_NAMES[activeAction];
let cancelled = false;
const poll = async () => {
try {
const resp = await api.getActionStatus(name);
if (cancelled) return;
setActionStatus(resp);
if (!resp.running) {
const ok = resp.exit_code === 0;
setToast({
type: ok ? "success" : "error",
message: ok
? t.status.actionFinished
: `${t.status.actionFailed} (exit ${resp.exit_code ?? "?"})`,
});
return;
}
} catch {
// transient fetch error; keep polling
}
if (!cancelled) setTimeout(poll, 1500);
};
poll();
return () => {
cancelled = true;
};
}, [activeAction, t.status.actionFinished, t.status.actionFailed]);
const runAction = useCallback(
async (action: SystemAction) => {
setPendingAction(action);
setActionStatus(null);
try {
if (action === "restart") {
await api.restartGateway();
} else {
await api.updateHermes();
}
setActiveAction(action);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
setToast({
type: "error",
message: `${t.status.actionFailed}: ${detail}`,
});
} finally {
setPendingAction(null);
}
},
[t.status.actionFailed],
);
const dismissLog = useCallback(() => {
setActiveAction(null);
setActionStatus(null);
}, []);
const isRunning = activeAction !== null && actionStatus?.running !== false;
const isBusy = pendingAction !== null || isRunning;
return (
<SystemActionsContext.Provider
value={{
actionStatus,
activeAction,
dismissLog,
isBusy,
isRunning,
pendingAction,
runAction,
}}
>
{children}
<Toast toast={toast} />
</SystemActionsContext.Provider>
);
}
interface ToastState {
message: string;
type: "success" | "error";
}