import { useCallback, useEffect, useRef, useState } from "react"; import { fetchHermesProfiles, fetchHermesStatus, fetchPluginSettings, updatePluginSettings, type HermesProfileSummary, type HermesProviderStatus, } from "../api"; import { RuntimeCardShell } from "./RuntimeCardShell"; const PLUGIN_ID = "fusion-plugin-hermes-runtime"; const HERMES_LEARN_MORE = "https://github.com/NousResearch/hermes-agent"; const HERMES_PROVIDER_OPTIONS = [ "auto", "anthropic", "openrouter", "gemini", "openai-codex", "copilot", "copilot-acp", "huggingface", "zai", "kimi-coding", "minimax", "minimax-cn", "kilocode", "xiaomi", "nous", ] as const; interface HermesSettings { binaryPath: string; model: string; provider: string; maxTurns: number; yolo: boolean; cliTimeoutMs: number; /** Hermes profile name; empty = "Auto / use Hermes default". */ profile: string; } const DEFAULT_SETTINGS: HermesSettings = { binaryPath: "", model: "", provider: "auto", maxTurns: 12, yolo: false, cliTimeoutMs: 300_000, profile: "", }; function settingsFromRecord(raw: Record): HermesSettings { return { binaryPath: typeof raw.binaryPath === "string" ? raw.binaryPath : DEFAULT_SETTINGS.binaryPath, model: typeof raw.model === "string" ? raw.model : DEFAULT_SETTINGS.model, provider: typeof raw.provider === "string" ? raw.provider : DEFAULT_SETTINGS.provider, maxTurns: typeof raw.maxTurns === "number" ? raw.maxTurns : DEFAULT_SETTINGS.maxTurns, yolo: typeof raw.yolo === "boolean" ? raw.yolo : DEFAULT_SETTINGS.yolo, cliTimeoutMs: typeof raw.cliTimeoutMs === "number" ? raw.cliTimeoutMs : DEFAULT_SETTINGS.cliTimeoutMs, profile: typeof raw.profile === "string" ? raw.profile : DEFAULT_SETTINGS.profile, }; } export function HermesRuntimeCard() { const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [status, setStatus] = useState(null); const [profiles, setProfiles] = useState([]); const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | null>(null); const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { setBusy("loading"); fetchPluginSettings(PLUGIN_ID) .then((raw) => { if (mountedRef.current) setSettings(settingsFromRecord(raw)); }) .catch(() => undefined) .finally(() => { if (mountedRef.current) setBusy(null); }); }, []); useEffect(() => { fetchHermesProfiles(settings.binaryPath ? { binaryPath: settings.binaryPath } : {}) .then((p) => { if (mountedRef.current) setProfiles(p); }) .catch(() => undefined); }, [settings.binaryPath]); const probe = useCallback(async (): Promise => { try { const next = await fetchHermesStatus( settings.binaryPath ? { binaryPath: settings.binaryPath } : {}, ); if (mountedRef.current) setStatus(next); return next; } catch (err) { if (mountedRef.current) { setToast({ kind: "err", message: err instanceof Error ? err.message : String(err), }); } return null; } }, [settings.binaryPath]); useEffect(() => { void probe(); }, [probe]); const buildPayload = useCallback( (): Record => ({ binaryPath: settings.binaryPath, model: settings.model, provider: settings.provider, maxTurns: settings.maxTurns, yolo: settings.yolo, cliTimeoutMs: settings.cliTimeoutMs, profile: settings.profile, }), [settings], ); const handleTest = useCallback(async () => { setBusy("testing"); setToast(null); const next = await probe(); if (!mountedRef.current) return; setBusy(null); if (!next) { setToast({ kind: "err", message: "Test failed — see status above." }); } else if (next.binary.available) { setToast({ kind: "ok", message: `✓ hermes detected${next.binary.version ? ` (${next.binary.version})` : ""}${next.binary.binaryPath ? ` at ${next.binary.binaryPath}` : ""}.`, }); } else { setToast({ kind: "err", message: `✗ ${next.binary.reason ?? "hermes not found"}`, }); } }, [probe]); const handleSave = useCallback(async () => { setBusy("saving"); setToast(null); try { await updatePluginSettings(PLUGIN_ID, buildPayload()); if (mountedRef.current) setToast({ kind: "ok", message: "Settings saved." }); } catch (err) { if (mountedRef.current) setToast({ kind: "err", message: err instanceof Error ? err.message : String(err) }); } finally { if (mountedRef.current) setBusy(null); } }, [buildPayload]); const handleSaveAndTest = useCallback(async () => { setBusy("save-test"); setToast(null); try { await updatePluginSettings(PLUGIN_ID, buildPayload()); const next = await probe(); if (!mountedRef.current) return; if (!next) { setToast({ kind: "err", message: "Saved, but probe failed." }); } else if (next.binary.available) { setToast({ kind: "ok", message: `Saved · ✓ hermes detected${next.binary.version ? ` (${next.binary.version})` : ""}.`, }); } else { setToast({ kind: "err", message: `Saved · ✗ ${next.binary.reason ?? "hermes not found"}`, }); } } catch (err) { if (mountedRef.current) setToast({ kind: "err", message: err instanceof Error ? err.message : String(err) }); } finally { if (mountedRef.current) setBusy(null); } }, [buildPayload, probe]); const binary = status?.binary; const statusKind = status === null ? "loading" : binary?.available ? "ok" : "err"; const statusText = status === null ? "Probing local hermes binary…" : binary?.available ? `✓ Detected${binary.version ? ` ${binary.version}` : ""}${binary.binaryPath ? ` · ${binary.binaryPath}` : ""}` : `✗ ${binary?.reason ?? "not detected on PATH"}`; return ( Nous Research } name="Hermes" subname="by Nous Research" learnMoreHref={HERMES_LEARN_MORE} statusKind={statusKind} statusText={statusText} description={ <> Drives the local hermes CLI as a subprocess. Each Fusion prompt is sent as hermes chat -q …; subsequent prompts resume the same hermes session via --resume. Provider, model, and skills are configured inside hermes itself; this card only chooses overrides. } busy={busy} toast={toast} onTest={() => void handleTest()} onSave={() => void handleSave()} onSaveAndTest={() => void handleSaveAndTest()} belowForm={ binary?.available === false ? (

hermes not detected. Install the upstream agent:

              pipx install hermes-agent
            

Hermes on GitHub

) : null } >
Select a Hermes profile to use. Activates the profile by setting{" "} HERMES_HOME to the profile directory when invoking{" "} hermes chat.
setSettings((s) => ({ ...s, binaryPath: e.target.value }))} /> Leave blank to resolve hermes from your PATH.
setSettings((s) => ({ ...s, model: e.target.value }))} /> {settings.profile ? ( Controlled by profile: {settings.profile} ) : ( Optional — overrides Hermes's configured default model. )}
{settings.profile ? ( Controlled by profile: {settings.profile} ) : ( Inference provider Hermes routes calls through (default: auto)). )}
setSettings((s) => ({ ...s, maxTurns: Number(e.target.value) || DEFAULT_SETTINGS.maxTurns, })) } /> Cap per Hermes turn. Hermes's own default is 90; we cap lower.
Required for non-interactive sessions that trigger shell-style tools.
setSettings((s) => ({ ...s, cliTimeoutMs: Number(e.target.value) || DEFAULT_SETTINGS.cliTimeoutMs, })) } /> Fusion-side hard cap. Default 5 min.
); }