Files
paperclip/ui/src/adapters/use-disabled-adapters.ts
T
HenkDz 4efe018a8f fix(ui): external adapter UI parser can now override builtin parsers
Builtin adapter types (hermes_local, openclaw_gateway, etc.) could not
be overridden by external adapters on the UI side. The registry always
returned the built-in parser, ignoring the external ui-parser.js shipped
by packages like hermes-paperclip-adapter.

Changes:
- registry.ts: full override lifecycle with generation guard for stale loads
- disabled-overrides-store.ts: client-side override pause state with
  useSyncExternalStore reactivity (persisted to localStorage)
- use-disabled-adapters.ts: subscribe to override store changes
- AdapterManager.tsx: separate controls for override pause (client-side)
  vs menu visibility (server-side), virtual builtin rows with badges
- adapters.ts: allow reload/reinstall of builtin types when overridden
2026-04-04 12:40:39 +01:00

60 lines
2.2 KiB
TypeScript

import { useEffect, useMemo, useSyncExternalStore } from "react";
import { useQuery } from "@tanstack/react-query";
import { adaptersApi } from "@/api/adapters";
import { setDisabledAdapterTypes } from "@/adapters/disabled-store";
import { isOverrideDisabled, subscribeToOverrides, getOverridesSnapshot } from "@/adapters/disabled-overrides-store";
import { syncExternalAdapters } from "@/adapters/registry";
import { queryKeys } from "@/lib/queryKeys";
/**
* Fetch adapters and keep the disabled-adapter store + UI adapter registry
* in sync with the server.
*
* - Registers external adapter types in the UI registry so they appear in
* dropdowns (done eagerly during render — idempotent, no React state).
* - Syncs the disabled-adapter store for non-React consumers (useEffect).
*
* Returns a reactive Set of disabled types for use as useMemo dependencies.
* Call this at the top of any component that renders adapter menus.
*/
export function useDisabledAdaptersSync(): Set<string> {
const { data: adapters } = useQuery({
queryKey: queryKeys.adapters.all,
queryFn: () => adaptersApi.list(),
staleTime: 5 * 60 * 1000,
});
// Subscribe to the client-side override store so that
// syncExternalAdapters() re-runs when overrides are toggled.
useSyncExternalStore(subscribeToOverrides, getOverridesSnapshot);
// Eagerly register external adapter types in the UI registry so that
// consumers calling listUIAdapters() in the same render cycle see them.
// This is idempotent — already-registered types are skipped.
if (adapters) {
syncExternalAdapters(
adapters
.filter((a) => a.source === "external")
.map((a) => ({
type: a.type,
label: a.label,
disabled: a.disabled,
overrideDisabled: a.overriddenBuiltin ? isOverrideDisabled(a.type) : undefined,
})),
);
}
// Sync the disabled set to the global store for non-React code
useEffect(() => {
if (!adapters) return;
setDisabledAdapterTypes(
adapters.filter((a) => a.disabled).map((a) => a.type),
);
}, [adapters]);
return useMemo(
() => new Set(adapters?.filter((a) => a.disabled).map((a) => a.type) ?? []),
[adapters],
);
}