Files
paperclip/ui/src/adapters/disabled-overrides-store.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

91 lines
2.8 KiB
TypeScript

/**
* Client-side store for disabled external adapter overrides.
*
* When an external adapter overrides a builtin type, the user may want to
* pause the override (use the builtin parser) without hiding the type from
* menus entirely. This is separate from the server's per-type `disabled`
* flag which controls menu visibility.
*
* Persisted to localStorage so it survives page reloads.
*
* Implements the React external store pattern (subscribe/getSnapshot)
* so that components using useSyncExternalStore re-render on changes.
*/
const STORAGE_KEY = "paperclip:disabled-overrides";
let disabledOverrides = new Set<string>();
// ── React external store plumbing ────────────────────────────────────
/** Monotonically increasing version — changes on every mutation. */
let snapshotVersion = 0;
const listeners = new Set<() => void>();
/** Subscribe to store changes (for useSyncExternalStore). */
export function subscribeToOverrides(callback: () => void): () => void {
listeners.add(callback);
return () => listeners.delete(callback);
}
/**
* Return a value that changes whenever the store changes.
* React compares this with Object.is to decide whether to re-render.
*/
export function getOverridesSnapshot(): number {
return snapshotVersion;
}
function emitChange(): void {
snapshotVersion++;
for (const fn of listeners) fn();
}
// ── Public API ───────────────────────────────────────────────────────
/** Check if the external override for a builtin type is paused. */
export function isOverrideDisabled(type: string): boolean {
return disabledOverrides.has(type);
}
/** Pause or resume an external override. */
export function setOverrideDisabled(type: string, disabled: boolean): void {
if (disabled) {
disabledOverrides.add(type);
} else {
disabledOverrides.delete(type);
}
persist();
emitChange();
}
/** Get all types with paused overrides (sync read). */
export function getDisabledOverrides(): Set<string> {
return disabledOverrides;
}
// ── Persistence ──────────────────────────────────────────────────────
function persist(): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...disabledOverrides]));
} catch {
// localStorage unavailable — no-op
}
}
function hydrate(): void {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
disabledOverrides = new Set(JSON.parse(raw));
}
} catch {
// corrupt or unavailable — start empty
}
}
// Hydrate on module load
hydrate();