hermes-agent/web/src/plugins/registry.ts
Austin Pickett edb2d91057
feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish (#28814)
* feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish

Replaces the hand-rolled shadcn-style `Checkbox` in `web/src/components/ui/`
with the Nous DS `Checkbox` (Radix-backed) from `@nous-research/ui`, bumps
the DS to 0.14.2, and picks up two regressions surfaced by the bump.

Checkbox migration
- bump `@nous-research/ui` 0.14.0 → ^0.14.2 and remove
  `web/src/components/ui/checkbox.tsx`
- migrate `ProfilesPage` and `ModelPickerDialog` to the DS Checkbox API
  (`onCheckedChange`, paired `<Label htmlFor>`)
- expose `Checkbox` on the dashboard plugin SDK
  (`web/src/plugins/registry.ts`) so plugin bundles can use the same
  DS component
- migrate the kanban dashboard plugin's 7 native `<input type="checkbox">`
  call sites to the SDK `Checkbox`, with a native-input fallback shim so
  the bundle still renders against older hosts that predate the SDK export

Fix: missing font registrations after the 0.14.x split
- import `@nous-research/ui/styles/fonts.css` before `globals.css` in
  `web/src/index.css`. As of 0.14.x, `globals.css` only declares the
  `--font-*` variables (Collapse, Mondwest, Rules Compressed/Expanded);
  the `@font-face` registrations now live in a separate `fonts.css`, so
  without this import the DS components silently fall back to a system
  font stack and look unstyled.

Fix: right-align page header toolbars on sm+ viewports
- The mobile dashboard polish in #28127 flipped four pages'
  `setEnd(...)` wrappers from `justify-end` to `w-full ... justify-start`
  so toolbars stack below the title and align left on small screens.
  But the outer `end` slot in `PageHeaderProvider` already has
  `sm:justify-end`, and that has no effect when its only child is
  `w-full` — once a flex child fills the row, the parent's `justify-*`
  can't move it. The toolbar pinned to the *left* of the right-side
  `sm:max-w-md` (~448px) slot, making the buttons appear to float a
  couple-hundred pixels off the right edge on Analytics, Models, Logs,
  and Plugins.
- Re-add `sm:justify-end` on the inner wrapper of each affected page,
  preserving the mobile stacked layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): update web npmDeps hash for package-lock bump

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): refresh npm lockfile hashes

* chore(ci): re-trigger checks after nix lockfile hash fix

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-20 08:00:17 -04:00

151 lines
4 KiB
TypeScript

/**
* Dashboard Plugin SDK + Registry
*
* Exposes React, UI components, hooks, and utilities on the window so
* that plugin bundles can use them without bundling their own copies.
*
* Plugins call window.__HERMES_PLUGINS__.register(name, Component)
* to register their tab component.
*/
import React, {
useState,
useEffect,
useCallback,
useMemo,
useRef,
useContext,
createContext,
} from "react";
import { api, fetchJSON } from "@/lib/api";
import { cn, timeAgo, isoTimeAgo } from "@/lib/utils";
import { Badge } from "@nous-research/ui/ui/components/badge";
import { Button } from "@nous-research/ui/ui/components/button";
import { Checkbox } from "@nous-research/ui/ui/components/checkbox";
import { Select, SelectOption } from "@nous-research/ui/ui/components/select";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsList, TabsTrigger } from "@nous-research/ui/ui/components/tabs";
import { useI18n } from "@/i18n";
import { registerSlot, PluginSlot } from "./slots";
// ---------------------------------------------------------------------------
// Plugin registry — plugins call register() to add their component.
// ---------------------------------------------------------------------------
type RegistryListener = () => void;
const _registered: Map<string, React.ComponentType> = new Map();
const _loadErrors: Map<string, string> = new Map();
const _listeners: Set<RegistryListener> = new Set();
function _notify() {
for (const fn of _listeners) {
try { fn(); } catch { /* ignore */ }
}
}
/** Re-run registry subscribers (e.g. after a plugin script onload, or dev HMR re-inject). */
export function notifyPluginRegistry() {
_notify();
}
/** Register a plugin component. Called by plugin JS bundles. */
function registerPlugin(name: string, component: React.ComponentType) {
_loadErrors.delete(name);
_registered.set(name, component);
_notify();
}
/** Get a registered component by plugin name. */
export function getPluginComponent(name: string): React.ComponentType | undefined {
return _registered.get(name);
}
export function getPluginLoadError(name: string): string | undefined {
return _loadErrors.get(name);
}
export function setPluginLoadError(name: string, message: string) {
_loadErrors.set(name, message);
_notify();
}
/** Subscribe to registry changes (returns unsubscribe fn). */
export function onPluginRegistered(fn: RegistryListener): () => void {
_listeners.add(fn);
return () => _listeners.delete(fn);
}
/** Get current count of registered plugins. */
export function getRegisteredCount(): number {
return _registered.size;
}
// ---------------------------------------------------------------------------
// Expose SDK + registry on window
// ---------------------------------------------------------------------------
declare global {
interface Window {
__HERMES_PLUGIN_SDK__: unknown;
__HERMES_PLUGINS__: {
register: typeof registerPlugin;
registerSlot: typeof registerSlot;
};
}
}
export function exposePluginSDK() {
window.__HERMES_PLUGINS__ = {
register: registerPlugin,
registerSlot,
};
window.__HERMES_PLUGIN_SDK__ = {
// React core — plugins use these instead of importing react
React,
hooks: {
useState,
useEffect,
useCallback,
useMemo,
useRef,
useContext,
createContext,
},
// Hermes API client
api,
// Raw fetchJSON for plugin-specific endpoints
fetchJSON,
// UI components — Nous DS where available, shadcn/ui primitives elsewhere.
components: {
Card,
CardHeader,
CardTitle,
CardContent,
Badge,
Button,
Checkbox,
Input,
Label,
Select,
SelectOption,
Separator,
Tabs,
TabsList,
TabsTrigger,
PluginSlot,
},
// Utilities
utils: { cn, timeAgo, isoTimeAgo },
// Hooks
useI18n,
};
}