mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 01:51:44 +00:00
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
/**
|
|
* usePlugins hook — discovers and loads dashboard plugins.
|
|
*
|
|
* 1. Fetches plugin manifests from GET /api/dashboard/plugins
|
|
* 2. Injects CSS <link> tags for plugins that declare css
|
|
* 3. Loads plugin JS bundles via <script> tags
|
|
* 4. Waits for plugins to call register() and resolves them
|
|
*/
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { api } from "@/lib/api";
|
|
import type { PluginManifest, RegisteredPlugin } from "./types";
|
|
import {
|
|
getPluginComponent,
|
|
onPluginRegistered,
|
|
notifyPluginRegistry,
|
|
setPluginLoadError,
|
|
} from "./registry";
|
|
|
|
export function usePlugins() {
|
|
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
|
const [plugins, setPlugins] = useState<RegisteredPlugin[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const loadedScripts = useRef<Set<string>>(new Set());
|
|
|
|
// Fetch manifests on mount.
|
|
useEffect(() => {
|
|
api
|
|
.getPlugins()
|
|
.then((list) => {
|
|
setManifests(list);
|
|
if (list.length === 0) setLoading(false);
|
|
})
|
|
.catch(() => setLoading(false));
|
|
}, []);
|
|
|
|
// Load plugin assets when manifests arrive.
|
|
useEffect(() => {
|
|
if (manifests.length === 0) return;
|
|
|
|
const injectedScripts: HTMLScriptElement[] = [];
|
|
|
|
for (const manifest of manifests) {
|
|
// Inject CSS if specified.
|
|
if (manifest.css) {
|
|
const cssUrl = `/dashboard-plugins/${manifest.name}/${manifest.css}`;
|
|
if (!document.querySelector(`link[href="${cssUrl}"]`)) {
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = cssUrl;
|
|
document.head.appendChild(link);
|
|
}
|
|
}
|
|
|
|
// Load JS bundle. In dev, cache-bust so Vite HMR can clear the
|
|
// in-memory registry while the browser would otherwise never
|
|
// re-execute a previously cached <script> URL.
|
|
const baseUrl = `/dashboard-plugins/${manifest.name}/${manifest.entry}`;
|
|
const scriptSrc = import.meta.env.DEV
|
|
? `${baseUrl}?hermes_dv=${Date.now()}`
|
|
: baseUrl;
|
|
if (!import.meta.env.DEV) {
|
|
if (loadedScripts.current.has(baseUrl)) continue;
|
|
loadedScripts.current.add(baseUrl);
|
|
}
|
|
|
|
const script = document.createElement("script");
|
|
script.setAttribute("data-hermes-plugin", manifest.name);
|
|
script.src = scriptSrc;
|
|
script.async = true;
|
|
script.onerror = () => {
|
|
setPluginLoadError(manifest.name, "LOAD_FAILED");
|
|
console.warn(
|
|
`[plugins] Failed to load ${manifest.name} from ${scriptSrc} (open Network tab)`,
|
|
);
|
|
};
|
|
script.onload = () => {
|
|
notifyPluginRegistry();
|
|
queueMicrotask(() => {
|
|
if (getPluginComponent(manifest.name)) return;
|
|
setPluginLoadError(manifest.name, "NO_REGISTER");
|
|
});
|
|
};
|
|
document.body.appendChild(script);
|
|
injectedScripts.push(script);
|
|
}
|
|
|
|
// Give plugins a moment to load and register, then stop loading state.
|
|
const timeout = setTimeout(() => setLoading(false), 2000);
|
|
return () => {
|
|
clearTimeout(timeout);
|
|
if (import.meta.env.DEV) {
|
|
for (const el of injectedScripts) {
|
|
el.remove();
|
|
}
|
|
}
|
|
};
|
|
}, [manifests]);
|
|
|
|
// Listen for plugin registrations and resolve them against manifests.
|
|
useEffect(() => {
|
|
function resolvePlugins() {
|
|
const resolved: RegisteredPlugin[] = [];
|
|
for (const manifest of manifests) {
|
|
const component = getPluginComponent(manifest.name);
|
|
if (component) {
|
|
resolved.push({ manifest, component });
|
|
}
|
|
}
|
|
setPlugins(resolved);
|
|
// If all plugins registered, stop loading early.
|
|
if (resolved.length === manifests.length && manifests.length > 0) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
resolvePlugins();
|
|
const unsub = onPluginRegistered(resolvePlugins);
|
|
return unsub;
|
|
}, [manifests]);
|
|
|
|
return { plugins, manifests, loading };
|
|
}
|