mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 03:30:39 +09:00
feat: server-side override pause/resume for builtin adapter types
Replace the client-side-only override store with a real server-side toggle. When a developer pauses the external override, the server swaps ALL adapter behavior back to the builtin — execute handler, model listing, config schema, detection — not just the UI parser. Server changes: - registry.ts: builtinFallbacks map + pausedOverrides set + setOverridePaused() - routes/adapters.ts: PATCH /api/adapters/:type/override endpoint + overridePaused in list UI changes: - adapters.ts: setOverridePaused API method + overridePaused on AdapterInfo - AdapterManager: overrideMutation calls server, instant feedback via invalidate() - use-disabled-adapters.ts: reads adapter.overridePaused from server response Removed: - disabled-overrides-store.ts: no longer needed (server is the source of truth) Note: already-running agent sessions keep the adapter they started with. Only new sessions use the swapped adapter.
This commit is contained in:
parent
4efe018a8f
commit
b81d765d2e
6 changed files with 127 additions and 126 deletions
|
|
@ -193,6 +193,15 @@ const hermesLocalAdapter: ServerAdapterModule = {
|
||||||
|
|
||||||
const adaptersByType = new Map<string, ServerAdapterModule>();
|
const adaptersByType = new Map<string, ServerAdapterModule>();
|
||||||
|
|
||||||
|
// For builtin types that are overridden by an external adapter, we keep the
|
||||||
|
// original builtin so it can be restored when the override is deactivated.
|
||||||
|
const builtinFallbacks = new Map<string, ServerAdapterModule>();
|
||||||
|
|
||||||
|
// Tracks which override types are currently deactivated (paused). When
|
||||||
|
// paused, `getServerAdapter()` returns the builtin fallback instead of the
|
||||||
|
// external. Persisted across reloads via the same disabled-adapters store.
|
||||||
|
const pausedOverrides = new Set<string>();
|
||||||
|
|
||||||
function registerBuiltInAdapters() {
|
function registerBuiltInAdapters() {
|
||||||
for (const adapter of [
|
for (const adapter of [
|
||||||
claudeLocalAdapter,
|
claudeLocalAdapter,
|
||||||
|
|
@ -237,8 +246,13 @@ const externalAdaptersReady: Promise<void> = (async () => {
|
||||||
const overriding = BUILTIN_ADAPTER_TYPES.has(externalAdapter.type);
|
const overriding = BUILTIN_ADAPTER_TYPES.has(externalAdapter.type);
|
||||||
if (overriding) {
|
if (overriding) {
|
||||||
console.log(
|
console.log(
|
||||||
`[paperclip] External adapter \"${externalAdapter.type}\" overrides built-in adapter`,
|
`[paperclip] External adapter "${externalAdapter.type}" overrides built-in adapter`,
|
||||||
);
|
);
|
||||||
|
// Save the original builtin for later restoration.
|
||||||
|
const existing = adaptersByType.get(externalAdapter.type);
|
||||||
|
if (existing && !builtinFallbacks.has(externalAdapter.type)) {
|
||||||
|
builtinFallbacks.set(externalAdapter.type, existing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
adaptersByType.set(
|
adaptersByType.set(
|
||||||
externalAdapter.type,
|
externalAdapter.type,
|
||||||
|
|
@ -281,6 +295,10 @@ export function requireServerAdapter(type: string): ServerAdapterModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServerAdapter(type: string): ServerAdapterModule {
|
export function getServerAdapter(type: string): ServerAdapterModule {
|
||||||
|
if (pausedOverrides.has(type)) {
|
||||||
|
const fallback = builtinFallbacks.get(type);
|
||||||
|
if (fallback) return fallback;
|
||||||
|
}
|
||||||
return adaptersByType.get(type) ?? processAdapter;
|
return adaptersByType.get(type) ?? processAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,6 +344,49 @@ export async function detectAdapterModel(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Override pause / resume
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause or resume an external override for a builtin adapter type.
|
||||||
|
*
|
||||||
|
* - `paused = true` → subsequent calls to `getServerAdapter(type)` return
|
||||||
|
* the builtin fallback instead of the external adapter. Already-running
|
||||||
|
* agent sessions are unaffected (they hold a reference to the module they
|
||||||
|
* started with).
|
||||||
|
*
|
||||||
|
* - `paused = false` → the external adapter is active again.
|
||||||
|
*
|
||||||
|
* Returns `true` if the state actually changed, `false` if the type is not
|
||||||
|
* an override or was already in the requested state.
|
||||||
|
*/
|
||||||
|
export function setOverridePaused(type: string, paused: boolean): boolean {
|
||||||
|
if (!builtinFallbacks.has(type)) return false;
|
||||||
|
const wasPaused = pausedOverrides.has(type);
|
||||||
|
if (paused && !wasPaused) {
|
||||||
|
pausedOverrides.add(type);
|
||||||
|
console.log(`[paperclip] Override paused for "${type}" — builtin adapter restored`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!paused && wasPaused) {
|
||||||
|
pausedOverrides.delete(type);
|
||||||
|
console.log(`[paperclip] Override resumed for "${type}" — external adapter active`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check whether the external override for a builtin type is currently paused. */
|
||||||
|
export function isOverridePaused(type: string): boolean {
|
||||||
|
return pausedOverrides.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the set of types whose overrides are currently paused. */
|
||||||
|
export function getPausedOverrides(): Set<string> {
|
||||||
|
return pausedOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
export function findServerAdapter(type: string): ServerAdapterModule | null {
|
export function findServerAdapter(type: string): ServerAdapterModule | null {
|
||||||
return adaptersByType.get(type) ?? null;
|
return adaptersByType.get(type) ?? null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import {
|
||||||
listEnabledServerAdapters,
|
listEnabledServerAdapters,
|
||||||
registerServerAdapter,
|
registerServerAdapter,
|
||||||
unregisterServerAdapter,
|
unregisterServerAdapter,
|
||||||
|
isOverridePaused,
|
||||||
|
setOverridePaused,
|
||||||
} from "../adapters/registry.js";
|
} from "../adapters/registry.js";
|
||||||
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||||
import {
|
import {
|
||||||
|
|
@ -65,6 +67,8 @@ interface AdapterInfo {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
/** True when an external plugin has replaced a built-in adapter of the same type. */
|
/** True when an external plugin has replaced a built-in adapter of the same type. */
|
||||||
overriddenBuiltin?: boolean;
|
overriddenBuiltin?: boolean;
|
||||||
|
/** True when the external override for a builtin type is currently paused. */
|
||||||
|
overridePaused?: boolean;
|
||||||
version?: string;
|
version?: string;
|
||||||
packageName?: string;
|
packageName?: string;
|
||||||
isLocalPath?: boolean;
|
isLocalPath?: boolean;
|
||||||
|
|
@ -108,6 +112,7 @@ function buildAdapterInfo(adapter: ServerAdapterModule, externalRecord: AdapterP
|
||||||
loaded: true, // If it's in the registry, it's loaded
|
loaded: true, // If it's in the registry, it's loaded
|
||||||
disabled: disabledSet.has(adapter.type),
|
disabled: disabledSet.has(adapter.type),
|
||||||
overriddenBuiltin: externalRecord ? BUILTIN_ADAPTER_TYPES.has(adapter.type) : undefined,
|
overriddenBuiltin: externalRecord ? BUILTIN_ADAPTER_TYPES.has(adapter.type) : undefined,
|
||||||
|
overridePaused: BUILTIN_ADAPTER_TYPES.has(adapter.type) ? isOverridePaused(adapter.type) : undefined,
|
||||||
// Prefer on-disk package.json so the UI reflects bumps without relying on store-only fields.
|
// Prefer on-disk package.json so the UI reflects bumps without relying on store-only fields.
|
||||||
version: fromDisk ?? externalRecord?.version,
|
version: fromDisk ?? externalRecord?.version,
|
||||||
packageName: externalRecord?.packageName,
|
packageName: externalRecord?.packageName,
|
||||||
|
|
@ -352,6 +357,37 @@ export function adapterRoutes() {
|
||||||
res.json({ type: adapterType, disabled, changed });
|
res.json({ type: adapterType, disabled, changed });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/adapters/:type/override
|
||||||
|
*
|
||||||
|
* Pause or resume an external adapter's override of a builtin type.
|
||||||
|
* When paused, the server returns the builtin adapter for all new requests
|
||||||
|
* (execute, listModels, config schema, etc.). Already-running sessions
|
||||||
|
* keep the adapter they started with.
|
||||||
|
*/
|
||||||
|
router.patch("/adapters/:type/override", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
|
||||||
|
const adapterType = req.params.type;
|
||||||
|
const { paused } = req.body as { paused?: boolean };
|
||||||
|
|
||||||
|
if (typeof paused !== "boolean") {
|
||||||
|
res.status(400).json({ error: "\"paused\" (boolean) is required in request body." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BUILTIN_ADAPTER_TYPES.has(adapterType)) {
|
||||||
|
res.status(400).json({ error: `Type "${adapterType}" is not a builtin adapter.` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = setOverridePaused(adapterType, paused);
|
||||||
|
|
||||||
|
logger.info({ type: adapterType, paused, changed }, "Adapter override toggle");
|
||||||
|
|
||||||
|
res.json({ type: adapterType, paused, changed });
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/adapters/:type
|
* DELETE /api/adapters/:type
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
/**
|
|
||||||
* 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,8 +1,7 @@
|
||||||
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { adaptersApi } from "@/api/adapters";
|
import { adaptersApi } from "@/api/adapters";
|
||||||
import { setDisabledAdapterTypes } from "@/adapters/disabled-store";
|
import { setDisabledAdapterTypes } from "@/adapters/disabled-store";
|
||||||
import { isOverrideDisabled, subscribeToOverrides, getOverridesSnapshot } from "@/adapters/disabled-overrides-store";
|
|
||||||
import { syncExternalAdapters } from "@/adapters/registry";
|
import { syncExternalAdapters } from "@/adapters/registry";
|
||||||
import { queryKeys } from "@/lib/queryKeys";
|
import { queryKeys } from "@/lib/queryKeys";
|
||||||
|
|
||||||
|
|
@ -24,10 +23,6 @@ export function useDisabledAdaptersSync(): Set<string> {
|
||||||
staleTime: 5 * 60 * 1000,
|
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
|
// Eagerly register external adapter types in the UI registry so that
|
||||||
// consumers calling listUIAdapters() in the same render cycle see them.
|
// consumers calling listUIAdapters() in the same render cycle see them.
|
||||||
// This is idempotent — already-registered types are skipped.
|
// This is idempotent — already-registered types are skipped.
|
||||||
|
|
@ -39,7 +34,7 @@ export function useDisabledAdaptersSync(): Set<string> {
|
||||||
type: a.type,
|
type: a.type,
|
||||||
label: a.label,
|
label: a.label,
|
||||||
disabled: a.disabled,
|
disabled: a.disabled,
|
||||||
overrideDisabled: a.overriddenBuiltin ? isOverrideDisabled(a.type) : undefined,
|
overrideDisabled: a.overridePaused,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ export interface AdapterInfo {
|
||||||
isLocalPath?: boolean;
|
isLocalPath?: boolean;
|
||||||
/** True when an external plugin has replaced a built-in adapter of the same type. */
|
/** True when an external plugin has replaced a built-in adapter of the same type. */
|
||||||
overriddenBuiltin?: boolean;
|
overriddenBuiltin?: boolean;
|
||||||
|
/** True when the external override for a builtin type is currently paused. */
|
||||||
|
overridePaused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdapterInstallResult {
|
export interface AdapterInstallResult {
|
||||||
|
|
@ -43,6 +45,10 @@ export const adaptersApi = {
|
||||||
setDisabled: (type: string, disabled: boolean) =>
|
setDisabled: (type: string, disabled: boolean) =>
|
||||||
api.patch<{ type: string; disabled: boolean; changed: boolean }>(`/adapters/${type}`, { disabled }),
|
api.patch<{ type: string; disabled: boolean; changed: boolean }>(`/adapters/${type}`, { disabled }),
|
||||||
|
|
||||||
|
/** Pause or resume an external override of a builtin type. */
|
||||||
|
setOverridePaused: (type: string, paused: boolean) =>
|
||||||
|
api.patch<{ type: string; paused: boolean; changed: boolean }>(`/adapters/${type}/override`, { paused }),
|
||||||
|
|
||||||
/** Reload an external adapter (bust server + client caches). */
|
/** Reload an external adapter (bust server + client caches). */
|
||||||
reload: (type: string) =>
|
reload: (type: string) =>
|
||||||
api.post<{ type: string; version?: string; reloaded: boolean }>(`/adapters/${type}/reload`, {}),
|
api.post<{ type: string; version?: string; reloaded: boolean }>(`/adapters/${type}/reload`, {}),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Adapters are simpler than plugins: no workers, no events, no manifests.
|
* Adapters are simpler than plugins: no workers, no events, no manifests.
|
||||||
* They just register a ServerAdapterModule that provides model discovery and execution.
|
* They just register a ServerAdapterModule that provides model discovery and execution.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlertTriangle, Cpu, Plus, Power, Trash2, FolderOpen, Package, RefreshCw, Download } from "lucide-react";
|
import { AlertTriangle, Cpu, Plus, Power, Trash2, FolderOpen, Package, RefreshCw, Download } from "lucide-react";
|
||||||
import { useCompany } from "@/context/CompanyContext";
|
import { useCompany } from "@/context/CompanyContext";
|
||||||
|
|
@ -32,7 +32,6 @@ import { cn } from "@/lib/utils";
|
||||||
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
||||||
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
||||||
import { invalidateConfigSchemaCache } from "@/adapters/schema-config-fields";
|
import { invalidateConfigSchemaCache } from "@/adapters/schema-config-fields";
|
||||||
import { isOverrideDisabled, setOverrideDisabled, subscribeToOverrides, getOverridesSnapshot } from "@/adapters/disabled-overrides-store";
|
|
||||||
|
|
||||||
function AdapterRow({
|
function AdapterRow({
|
||||||
adapter,
|
adapter,
|
||||||
|
|
@ -258,11 +257,6 @@ export function AdapterManager() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { pushToast } = useToast();
|
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 [installPackage, setInstallPackage] = useState("");
|
||||||
const [installVersion, setInstallVersion] = useState("");
|
const [installVersion, setInstallVersion] = useState("");
|
||||||
const [isLocalPath, setIsLocalPath] = useState(false);
|
const [isLocalPath, setIsLocalPath] = useState(false);
|
||||||
|
|
@ -309,9 +303,7 @@ export function AdapterManager() {
|
||||||
|
|
||||||
const removeMutation = useMutation({
|
const removeMutation = useMutation({
|
||||||
mutationFn: (type: string) => adaptersApi.remove(type),
|
mutationFn: (type: string) => adaptersApi.remove(type),
|
||||||
onSuccess: (_result, type) => {
|
onSuccess: () => {
|
||||||
// Clean up client-side override state when the external is removed.
|
|
||||||
setOverrideDisabled(type, false);
|
|
||||||
invalidate();
|
invalidate();
|
||||||
pushToast({ title: "Adapter removed", tone: "success" });
|
pushToast({ title: "Adapter removed", tone: "success" });
|
||||||
},
|
},
|
||||||
|
|
@ -331,6 +323,17 @@ export function AdapterManager() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const overrideMutation = useMutation({
|
||||||
|
mutationFn: ({ type, paused }: { type: string; paused: boolean }) =>
|
||||||
|
adaptersApi.setOverridePaused(type, paused),
|
||||||
|
onSuccess: () => {
|
||||||
|
invalidate();
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
pushToast({ title: "Override toggle failed", body: err.message, tone: "error" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const reloadMutation = useMutation({
|
const reloadMutation = useMutation({
|
||||||
mutationFn: (type: string) => adaptersApi.reload(type),
|
mutationFn: (type: string) => adaptersApi.reload(type),
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
|
|
@ -371,8 +374,6 @@ export function AdapterManager() {
|
||||||
// External adapters that override a builtin type. The server only returns
|
// External adapters that override a builtin type. The server only returns
|
||||||
// one entry per type (the external), so we synthesize a builtin row for
|
// 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 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 ?? [])
|
const overriddenBuiltins = (adapters ?? [])
|
||||||
.filter((a) => a.source === "external" && a.overriddenBuiltin)
|
.filter((a) => a.source === "external" && a.overriddenBuiltin)
|
||||||
.filter((a) => !builtinAdapters.some((b) => b.type === a.type))
|
.filter((a) => !builtinAdapters.some((b) => b.type === a.type))
|
||||||
|
|
@ -383,15 +384,13 @@ export function AdapterManager() {
|
||||||
a.packageName,
|
a.packageName,
|
||||||
a.version ? `v${a.version}` : undefined,
|
a.version ? `v${a.version}` : undefined,
|
||||||
].filter(Boolean).join(" "),
|
].filter(Boolean).join(" "),
|
||||||
// The override-paused state is client-side and independent of
|
overridePaused: !!a.overridePaused,
|
||||||
// the type's server-side menu visibility.
|
menuDisabled: !!a.disabled,
|
||||||
overridePaused: isOverrideDisabled(a.type),
|
|
||||||
menuDisabled: a.disabled ?? false,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (isLoading) return <div className="p-4 text-sm text-muted-foreground">Loading adapters...</div>;
|
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;
|
const isMutating = installMutation.isPending || removeMutation.isPending || toggleMutation.isPending || overrideMutation.isPending || reloadMutation.isPending || reinstallMutation.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-5xl">
|
<div className="space-y-6 max-w-5xl">
|
||||||
|
|
@ -546,12 +545,12 @@ export function AdapterManager() {
|
||||||
<ul className="divide-y rounded-md border bg-card">
|
<ul className="divide-y rounded-md border bg-card">
|
||||||
{externalAdapters.map((adapter) => {
|
{externalAdapters.map((adapter) => {
|
||||||
const isBuiltinOverride = adapter.overriddenBuiltin;
|
const isBuiltinOverride = adapter.overriddenBuiltin;
|
||||||
const overridePaused = isBuiltinOverride && isOverrideDisabled(adapter.type);
|
const overridePaused = isBuiltinOverride && !!adapter.overridePaused;
|
||||||
|
|
||||||
// For overridden builtins, the power button controls the
|
// For overridden builtins, the power button controls the
|
||||||
// client-side override state (not server menu visibility).
|
// override pause state (not server menu visibility).
|
||||||
const effectiveAdapter: AdapterInfo = isBuiltinOverride
|
const effectiveAdapter: AdapterInfo = isBuiltinOverride
|
||||||
? { ...adapter, disabled: !!overridePaused }
|
? { ...adapter, disabled: overridePaused }
|
||||||
: adapter;
|
: adapter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -561,19 +560,13 @@ export function AdapterManager() {
|
||||||
canRemove={true}
|
canRemove={true}
|
||||||
onToggle={
|
onToggle={
|
||||||
isBuiltinOverride
|
isBuiltinOverride
|
||||||
? (type, disabled) => {
|
? (type, disabled) => overrideMutation.mutate({ type, paused: 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 })
|
: (type, disabled) => toggleMutation.mutate({ type, disabled })
|
||||||
}
|
}
|
||||||
onRemove={(type) => setRemoveType(type)}
|
onRemove={(type) => setRemoveType(type)}
|
||||||
onReload={(type) => reloadMutation.mutate(type)}
|
onReload={(type) => reloadMutation.mutate(type)}
|
||||||
onReinstall={!adapter.isLocalPath ? (type) => setReinstallTarget(adapter) : undefined}
|
onReinstall={!adapter.isLocalPath ? (type) => setReinstallTarget(adapter) : undefined}
|
||||||
isToggling={isBuiltinOverride ? false : toggleMutation.isPending}
|
isToggling={isBuiltinOverride ? overrideMutation.isPending : toggleMutation.isPending}
|
||||||
isReloading={reloadMutation.isPending}
|
isReloading={reloadMutation.isPending}
|
||||||
isReinstalling={reinstallMutation.isPending}
|
isReinstalling={reinstallMutation.isPending}
|
||||||
toggleTitleDisabled={isBuiltinOverride ? "Pause external override" : undefined}
|
toggleTitleDisabled={isBuiltinOverride ? "Pause external override" : undefined}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue