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
This commit is contained in:
@@ -437,8 +437,8 @@ export function adapterRoutes() {
|
||||
|
||||
const type = req.params.type;
|
||||
|
||||
// Built-in adapters cannot be reloaded
|
||||
if (BUILTIN_ADAPTER_TYPES.has(type)) {
|
||||
// Built-in adapters cannot be reloaded unless overridden by an external one
|
||||
if (BUILTIN_ADAPTER_TYPES.has(type) && !getAdapterPluginByType(type)) {
|
||||
res.status(400).json({ error: "Cannot reload built-in adapter." });
|
||||
return;
|
||||
}
|
||||
@@ -489,7 +489,7 @@ export function adapterRoutes() {
|
||||
|
||||
const type = req.params.type;
|
||||
|
||||
if (BUILTIN_ADAPTER_TYPES.has(type)) {
|
||||
if (BUILTIN_ADAPTER_TYPES.has(type) && !getAdapterPluginByType(type)) {
|
||||
res.status(400).json({ error: "Cannot reinstall built-in adapter." });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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();
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { UIAdapterModule } from "../types";
|
||||
import { parseHermesStdoutLine } from "hermes-paperclip-adapter/ui";
|
||||
import { HermesLocalConfigFields } from "./config-fields";
|
||||
import { SchemaConfigFields, buildSchemaAdapterConfig } from "../schema-config-fields";
|
||||
import { buildHermesConfig } from "hermes-paperclip-adapter/ui";
|
||||
|
||||
export const hermesLocalUIAdapter: UIAdapterModule = {
|
||||
type: "hermes_local",
|
||||
label: "Hermes Agent",
|
||||
parseStdoutLine: parseHermesStdoutLine,
|
||||
ConfigFields: HermesLocalConfigFields,
|
||||
buildAdapterConfig: buildHermesConfig,
|
||||
ConfigFields: SchemaConfigFields,
|
||||
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||
};
|
||||
|
||||
+119
-13
@@ -9,12 +9,28 @@ import { openClawGatewayUIAdapter } from "./openclaw-gateway";
|
||||
import { hermesLocalUIAdapter } from "./hermes-local";
|
||||
import { processUIAdapter } from "./process";
|
||||
import { httpUIAdapter } from "./http";
|
||||
import { loadDynamicParser } from "./dynamic-loader";
|
||||
import { loadDynamicParser, invalidateDynamicParser } from "./dynamic-loader";
|
||||
import { SchemaConfigFields, buildSchemaAdapterConfig } from "./schema-config-fields";
|
||||
|
||||
const uiAdapters: UIAdapterModule[] = [];
|
||||
const adaptersByType = new Map<string, UIAdapterModule>();
|
||||
|
||||
// Types registered at module load time — allowed to be overridden by
|
||||
// external adapters that ship their own ui-parser.js via the server.
|
||||
const builtinTypes = new Set<string>();
|
||||
|
||||
// Original builtin adapters stored for restoration when external overrides
|
||||
// are deactivated or removed.
|
||||
const builtinAdaptersByType = new Map<string, UIAdapterModule>();
|
||||
|
||||
// Tracks which builtin types currently have an active external override.
|
||||
const activeExternalOverrides = new Set<string>();
|
||||
|
||||
// Generation counter to discard stale dynamic parser loads. When an override
|
||||
// is deactivated while a load is in-flight, the generation is bumped and the
|
||||
// stale result is discarded in its .then() handler.
|
||||
const overrideGeneration = new Map<string, number>();
|
||||
|
||||
// Subscriber list — components can register to be notified when adapters change
|
||||
// (e.g., when a dynamic parser replaces a placeholder).
|
||||
const adapterChangeListeners = new Set<() => void>();
|
||||
@@ -42,6 +58,8 @@ function registerBuiltInUIAdapters() {
|
||||
processUIAdapter,
|
||||
httpUIAdapter,
|
||||
]) {
|
||||
builtinTypes.add(adapter.type);
|
||||
builtinAdaptersByType.set(adapter.type, adapter);
|
||||
registerUIAdapter(adapter);
|
||||
}
|
||||
}
|
||||
@@ -106,20 +124,108 @@ export function getUIAdapter(type: string): UIAdapterModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure external adapter types (from the server's /api/adapters response)
|
||||
* are registered in the UI adapter list so they appear in dropdowns.
|
||||
* Keep the UI adapter registry in sync with the server's adapter list.
|
||||
*
|
||||
* For each type not already registered, creates a placeholder module that
|
||||
* uses the process adapter defaults and kicks off dynamic parser loading.
|
||||
* Once the parser resolves, the placeholder is replaced with the real one.
|
||||
* Two concerns:
|
||||
*
|
||||
* 1. **Builtin overrides** — when an external adapter ships a ui-parser.js for a
|
||||
* builtin type, the external parser takes priority. When the external is
|
||||
* disabled or removed the original builtin parser is restored transparently.
|
||||
* A generation counter guards against stale loads that resolve after the
|
||||
* override has been torn down.
|
||||
*
|
||||
* 2. **Non-builtin externals** — register a bridge adapter that lazily loads the
|
||||
* dynamic parser on first stdout line, falling back to the generic process
|
||||
* adapter. Once the parser resolves the bridge is replaced.
|
||||
*/
|
||||
export function syncExternalAdapters(
|
||||
serverAdapters: { type: string; label: string }[],
|
||||
serverAdapters: {
|
||||
type: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
/** When true, the external override for a builtin type is client-side paused. */
|
||||
overrideDisabled?: boolean;
|
||||
}[],
|
||||
): void {
|
||||
const enabledExternalTypes = new Set(
|
||||
serverAdapters.filter((a) => !a.disabled && !a.overrideDisabled).map((a) => a.type),
|
||||
);
|
||||
const allExternalTypes = new Set(
|
||||
serverAdapters.map((a) => a.type),
|
||||
);
|
||||
|
||||
// ── Builtin override lifecycle ──────────────────────────────────────────
|
||||
|
||||
for (const builtinType of builtinTypes) {
|
||||
const originalBuiltin = builtinAdaptersByType.get(builtinType);
|
||||
if (!originalBuiltin) continue;
|
||||
|
||||
const hasExternal = allExternalTypes.has(builtinType);
|
||||
const externalEnabled = enabledExternalTypes.has(builtinType);
|
||||
const wasOverridden = activeExternalOverrides.has(builtinType);
|
||||
|
||||
if (hasExternal && externalEnabled && !wasOverridden) {
|
||||
// Activate: external just became active → replace builtin with bridge.
|
||||
activeExternalOverrides.add(builtinType);
|
||||
|
||||
const gen = (overrideGeneration.get(builtinType) ?? 0) + 1;
|
||||
overrideGeneration.set(builtinType, gen);
|
||||
|
||||
let loadStarted = false;
|
||||
const fallbackParser = originalBuiltin.parseStdoutLine;
|
||||
const externalEntry = serverAdapters.find((a) => a.type === builtinType);
|
||||
const label = externalEntry?.label ?? builtinType;
|
||||
|
||||
registerUIAdapter({
|
||||
type: builtinType,
|
||||
label,
|
||||
parseStdoutLine: (line: string, ts: string) => {
|
||||
if (!loadStarted) {
|
||||
loadStarted = true;
|
||||
loadDynamicParser(builtinType).then((parser) => {
|
||||
// Discard if the override was torn down while the load was in-flight.
|
||||
if (parser && overrideGeneration.get(builtinType) === gen) {
|
||||
registerUIAdapter({
|
||||
type: builtinType,
|
||||
label,
|
||||
parseStdoutLine: parser,
|
||||
ConfigFields: originalBuiltin.ConfigFields,
|
||||
buildAdapterConfig: originalBuiltin.buildAdapterConfig,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return fallbackParser(line, ts);
|
||||
},
|
||||
ConfigFields: originalBuiltin.ConfigFields,
|
||||
buildAdapterConfig: originalBuiltin.buildAdapterConfig,
|
||||
});
|
||||
} else if ((!hasExternal || !externalEnabled) && wasOverridden) {
|
||||
// Deactivate: external disabled or removed → restore builtin.
|
||||
activeExternalOverrides.delete(builtinType);
|
||||
overrideGeneration.delete(builtinType);
|
||||
invalidateDynamicParser(builtinType);
|
||||
registerUIAdapter(originalBuiltin);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Non-builtin externals ───────────────────────────────────────────────
|
||||
|
||||
for (const { type, label } of serverAdapters) {
|
||||
if (adaptersByType.has(type)) continue;
|
||||
if (builtinTypes.has(type)) continue; // handled above
|
||||
|
||||
const existing = adaptersByType.get(type);
|
||||
|
||||
// If this type already has an externally-loaded dynamic parser, skip —
|
||||
// it was loaded from disk on a previous sync. Only re-trigger loading
|
||||
// when the server returns a new external adapter that hasn't been loaded yet.
|
||||
if (existing && existing !== processUIAdapter) continue;
|
||||
|
||||
let loadStarted = false;
|
||||
// Use the existing built-in parser as fallback (if any) so we don't
|
||||
// regress to the generic process parser while the dynamic one loads.
|
||||
const fallbackParser = existing?.parseStdoutLine ?? processUIAdapter.parseStdoutLine;
|
||||
|
||||
registerUIAdapter({
|
||||
type,
|
||||
label,
|
||||
@@ -132,16 +238,16 @@ export function syncExternalAdapters(
|
||||
type,
|
||||
label,
|
||||
parseStdoutLine: parser,
|
||||
ConfigFields: SchemaConfigFields,
|
||||
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||
ConfigFields: existing?.ConfigFields ?? SchemaConfigFields,
|
||||
buildAdapterConfig: existing?.buildAdapterConfig ?? buildSchemaAdapterConfig,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return processUIAdapter.parseStdoutLine(line, ts);
|
||||
return fallbackParser(line, ts);
|
||||
},
|
||||
ConfigFields: SchemaConfigFields,
|
||||
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||
ConfigFields: existing?.ConfigFields ?? SchemaConfigFields,
|
||||
buildAdapterConfig: existing?.buildAdapterConfig ?? buildSchemaAdapterConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
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";
|
||||
|
||||
@@ -23,6 +24,10 @@ export function useDisabledAdaptersSync(): Set<string> {
|
||||
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.
|
||||
@@ -30,7 +35,12 @@ export function useDisabledAdaptersSync(): Set<string> {
|
||||
syncExternalAdapters(
|
||||
adapters
|
||||
.filter((a) => a.source === "external")
|
||||
.map((a) => ({ type: a.type, label: a.label })),
|
||||
.map((a) => ({
|
||||
type: a.type,
|
||||
label: a.label,
|
||||
disabled: a.disabled,
|
||||
overrideDisabled: a.overriddenBuiltin ? isOverrideDisabled(a.type) : undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+109
-19
@@ -4,7 +4,7 @@
|
||||
* Adapters are simpler than plugins: no workers, no events, no manifests.
|
||||
* They just register a ServerAdapterModule that provides model discovery and execution.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Cpu, Plus, Power, Trash2, FolderOpen, Package, RefreshCw, Download } from "lucide-react";
|
||||
import { useCompany } from "@/context/CompanyContext";
|
||||
@@ -32,6 +32,7 @@ import { cn } from "@/lib/utils";
|
||||
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
||||
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
||||
import { invalidateConfigSchemaCache } from "@/adapters/schema-config-fields";
|
||||
import { isOverrideDisabled, setOverrideDisabled, subscribeToOverrides, getOverridesSnapshot } from "@/adapters/disabled-overrides-store";
|
||||
|
||||
function AdapterRow({
|
||||
adapter,
|
||||
@@ -43,6 +44,13 @@ function AdapterRow({
|
||||
isToggling,
|
||||
isReloading,
|
||||
isReinstalling,
|
||||
overriddenBy,
|
||||
/** Custom tooltip for the power button when adapter is enabled. */
|
||||
toggleTitleEnabled,
|
||||
/** Custom tooltip for the power button when adapter is disabled. */
|
||||
toggleTitleDisabled,
|
||||
/** Custom label for the disabled badge (defaults to "Hidden from menus"). */
|
||||
disabledBadgeLabel,
|
||||
}: {
|
||||
adapter: AdapterInfo;
|
||||
canRemove: boolean;
|
||||
@@ -53,6 +61,11 @@ function AdapterRow({
|
||||
isToggling: boolean;
|
||||
isReloading?: boolean;
|
||||
isReinstalling?: boolean;
|
||||
/** When set, shows an "Overridden by …" badge (used for builtin entries). */
|
||||
overriddenBy?: string;
|
||||
toggleTitleEnabled?: string;
|
||||
toggleTitleDisabled?: string;
|
||||
disabledBadgeLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<li>
|
||||
@@ -78,9 +91,14 @@ function AdapterRow({
|
||||
Overrides built-in
|
||||
</Badge>
|
||||
)}
|
||||
{overriddenBy && (
|
||||
<Badge variant="secondary" className="text-blue-600 border-blue-400">
|
||||
Overridden by {overriddenBy}
|
||||
</Badge>
|
||||
)}
|
||||
{adapter.disabled && (
|
||||
<Badge variant="secondary" className="text-amber-600 border-amber-400">
|
||||
Hidden from menus
|
||||
{disabledBadgeLabel ?? "Hidden from menus"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -121,7 +139,9 @@ function AdapterRow({
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="h-8 w-8"
|
||||
title={adapter.disabled ? "Show in agent menus" : "Hide from agent menus"}
|
||||
title={adapter.disabled
|
||||
? (toggleTitleEnabled ?? "Show in agent menus")
|
||||
: (toggleTitleDisabled ?? "Hide from agent menus")}
|
||||
disabled={isToggling}
|
||||
onClick={() => onToggle(adapter.type, !adapter.disabled)}
|
||||
>
|
||||
@@ -238,6 +258,11 @@ export function AdapterManager() {
|
||||
const queryClient = useQueryClient();
|
||||
const { pushToast } = useToast();
|
||||
|
||||
// Subscribe to client-side override store so the component re-renders
|
||||
// immediately when setOverrideDisabled() is called, even though the
|
||||
// server query data hasn't changed.
|
||||
useSyncExternalStore(subscribeToOverrides, getOverridesSnapshot);
|
||||
|
||||
const [installPackage, setInstallPackage] = useState("");
|
||||
const [installVersion, setInstallVersion] = useState("");
|
||||
const [isLocalPath, setIsLocalPath] = useState(false);
|
||||
@@ -284,7 +309,9 @@ export function AdapterManager() {
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: (type: string) => adaptersApi.remove(type),
|
||||
onSuccess: () => {
|
||||
onSuccess: (_result, type) => {
|
||||
// Clean up client-side override state when the external is removed.
|
||||
setOverrideDisabled(type, false);
|
||||
invalidate();
|
||||
pushToast({ title: "Adapter removed", tone: "success" });
|
||||
},
|
||||
@@ -341,6 +368,27 @@ export function AdapterManager() {
|
||||
const builtinAdapters = (adapters ?? []).filter((a) => a.source === "builtin");
|
||||
const externalAdapters = (adapters ?? []).filter((a) => a.source === "external");
|
||||
|
||||
// External adapters that override a builtin type. The server only returns
|
||||
// one entry per type (the external), so we synthesize a builtin row for
|
||||
// the builtins section so users can see which builtins are affected.
|
||||
// The virtual entry's disabled state reflects the TYPE's menu visibility
|
||||
// (server-side disabled flag), NOT the external adapter's override state.
|
||||
const overriddenBuiltins = (adapters ?? [])
|
||||
.filter((a) => a.source === "external" && a.overriddenBuiltin)
|
||||
.filter((a) => !builtinAdapters.some((b) => b.type === a.type))
|
||||
.map((a) => ({
|
||||
type: a.type,
|
||||
label: getAdapterLabel(a.type),
|
||||
overriddenBy: [
|
||||
a.packageName,
|
||||
a.version ? `v${a.version}` : undefined,
|
||||
].filter(Boolean).join(" "),
|
||||
// The override-paused state is client-side and independent of
|
||||
// the type's server-side menu visibility.
|
||||
overridePaused: isOverrideDisabled(a.type),
|
||||
menuDisabled: a.disabled ?? false,
|
||||
}));
|
||||
|
||||
if (isLoading) return <div className="p-4 text-sm text-muted-foreground">Loading adapters...</div>;
|
||||
|
||||
const isMutating = installMutation.isPending || removeMutation.isPending || toggleMutation.isPending || reloadMutation.isPending || reinstallMutation.isPending;
|
||||
@@ -496,20 +544,44 @@ export function AdapterManager() {
|
||||
</Card>
|
||||
) : (
|
||||
<ul className="divide-y rounded-md border bg-card">
|
||||
{externalAdapters.map((adapter) => (
|
||||
<AdapterRow
|
||||
key={adapter.type}
|
||||
adapter={adapter}
|
||||
canRemove={true}
|
||||
onToggle={(type, disabled) => toggleMutation.mutate({ type, disabled })}
|
||||
onRemove={(type) => setRemoveType(type)}
|
||||
onReload={(type) => reloadMutation.mutate(type)}
|
||||
onReinstall={!adapter.isLocalPath ? (type) => setReinstallTarget(adapter) : undefined}
|
||||
isToggling={toggleMutation.isPending}
|
||||
isReloading={reloadMutation.isPending}
|
||||
isReinstalling={reinstallMutation.isPending}
|
||||
/>
|
||||
))}
|
||||
{externalAdapters.map((adapter) => {
|
||||
const isBuiltinOverride = adapter.overriddenBuiltin;
|
||||
const overridePaused = isBuiltinOverride && isOverrideDisabled(adapter.type);
|
||||
|
||||
// For overridden builtins, the power button controls the
|
||||
// client-side override state (not server menu visibility).
|
||||
const effectiveAdapter: AdapterInfo = isBuiltinOverride
|
||||
? { ...adapter, disabled: !!overridePaused }
|
||||
: adapter;
|
||||
|
||||
return (
|
||||
<AdapterRow
|
||||
key={adapter.type}
|
||||
adapter={effectiveAdapter}
|
||||
canRemove={true}
|
||||
onToggle={
|
||||
isBuiltinOverride
|
||||
? (type, disabled) => {
|
||||
setOverrideDisabled(type, disabled);
|
||||
// useSyncExternalStore handles the re-render;
|
||||
// also invalidate so other components (e.g. menus)
|
||||
// eventually pick up the registry change.
|
||||
invalidate();
|
||||
}
|
||||
: (type, disabled) => toggleMutation.mutate({ type, disabled })
|
||||
}
|
||||
onRemove={(type) => setRemoveType(type)}
|
||||
onReload={(type) => reloadMutation.mutate(type)}
|
||||
onReinstall={!adapter.isLocalPath ? (type) => setReinstallTarget(adapter) : undefined}
|
||||
isToggling={isBuiltinOverride ? false : toggleMutation.isPending}
|
||||
isReloading={reloadMutation.isPending}
|
||||
isReinstalling={reinstallMutation.isPending}
|
||||
toggleTitleDisabled={isBuiltinOverride ? "Pause external override" : undefined}
|
||||
toggleTitleEnabled={isBuiltinOverride ? "Resume external override" : undefined}
|
||||
disabledBadgeLabel={isBuiltinOverride ? "Override paused" : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
@@ -521,7 +593,7 @@ export function AdapterManager() {
|
||||
<h2 className="text-base font-semibold">Built-in Adapters</h2>
|
||||
</div>
|
||||
|
||||
{builtinAdapters.length === 0 ? (
|
||||
{builtinAdapters.length === 0 && overriddenBuiltins.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">No built-in adapters found.</div>
|
||||
) : (
|
||||
<ul className="divide-y rounded-md border bg-card">
|
||||
@@ -535,6 +607,24 @@ export function AdapterManager() {
|
||||
isToggling={isMutating}
|
||||
/>
|
||||
))}
|
||||
{overriddenBuiltins.map((virtual) => (
|
||||
<AdapterRow
|
||||
key={virtual.type}
|
||||
adapter={{
|
||||
type: virtual.type,
|
||||
label: virtual.label,
|
||||
source: "builtin",
|
||||
modelsCount: 0,
|
||||
loaded: true,
|
||||
disabled: virtual.menuDisabled,
|
||||
}}
|
||||
canRemove={false}
|
||||
onToggle={(type, disabled) => toggleMutation.mutate({ type, disabled })}
|
||||
onRemove={() => {}}
|
||||
isToggling={isMutating}
|
||||
overriddenBy={virtual.overridePaused ? undefined : virtual.overriddenBy}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user