FN-7629: add enable/disable control for built-in runtime plugins
Adds a durable Plugin Manager toggle to enable/disable built-in runtime plugins (Hermes, Paperclip, OpenClaw, Droid) that persists across restarts, replacing the dead-end "Built-in metadata only" CTA. - renderBuiltinPluginSection now renders an enable/disable toggle for runtime built-ins regardless of installed status - Disabling a not-yet-installed built-in first installs it (mirroring CLI's ensureBundledPluginInstalled) then immediately disables it, so a plugin_installs row + disabled project state exists with no new persistence primitive needed - HermesRuntimeCard/OpenClawRuntimeCard/PaperclipRuntimeCard now show "Disabled in Plugin Manager" instead of a stale detected/connected status when disabled - Added i18n strings across all locales and updated docs - Added changeset for @runfusion/fusion (minor) - Expanded plugin-loader and PluginManager test coverage for the new disable/enable flows Files changed: .changeset/fn-7629-builtin-runtime-disable.md | 7 + docs/dashboard-guide.md | 1 + docs/plugin-management.md | 4 + packages/core/src/__tests__/plugin-loader.test.ts | 46 ++++++ .../dashboard/app/components/HermesRuntimeCard.tsx | 40 ++++- .../app/components/OpenClawRuntimeCard.tsx | 32 +++- .../app/components/PaperclipRuntimeCard.tsx | 32 +++- .../dashboard/app/components/PluginManager.css | 34 +++++ .../dashboard/app/components/PluginManager.tsx | 167 +++++++++++++++------ .../components/__tests__/PluginManager.test.tsx | 12 +- .../__tests__/PluginManager.toggle.test.tsx | 104 ++++++++++++- packages/i18n/locales/en/app.json | 4 + packages/i18n/locales/es/app.json | 4 + packages/i18n/locales/fr/app.json | 4 + packages/i18n/locales/ko/app.json | 4 + packages/i18n/locales/zh-CN/app.json | 4 + packages/i18n/locales/zh-TW/app.json | 4 + packages/i18n/src/resources.d.ts | 4 + 18 files changed, 440 insertions(+), 67 deletions(-) Fusion-Task-Id: FN-7629 Fusion-Task-Lineage: 2a25bb1f-3f73-4273-8769-af05c61778f9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7629-builtin-runtime-disable.md
Normal file
7
.changeset/fn-7629-builtin-runtime-disable.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Built-in runtime plugins (Hermes, Paperclip, OpenClaw, Droid) can now be disabled and stay disabled across restarts.
|
||||||
|
category: feature
|
||||||
|
dev: renderBuiltinPluginSection now renders a durable enable/disable toggle for runtime built-ins independent of installed status, replacing the dead-end "Built-in metadata only" CTA for the not-installed / activated-without-record case. Chosen persistence path: on disable, a not-yet-installed built-in runtime is first registered via the existing installPlugin path (mirroring the CLI's ensureBundledPluginInstalled lazy-install), then disablePlugin is called immediately so a plugin_installs row + project state exists with enabled=false — no new persistence primitive needed since loadAllPlugins/loadPlugin already skip disabled plugins and recordActivationEvent only fires on actual load, so a disabled runtime is never re-activated on restart. HermesRuntimeCard/OpenClawRuntimeCard/PaperclipRuntimeCard now reflect the Plugin Manager disabled state ("Disabled in Plugin Manager") instead of showing a stale detected/connected status.
|
||||||
@@ -1157,6 +1157,7 @@ Features:
|
|||||||
- Enable/disable plugins, reload active plugins, and uninstall plugins
|
- Enable/disable plugins, reload active plugins, and uninstall plugins
|
||||||
- Inspect plugin runtime state and transition feedback
|
- Inspect plugin runtime state and transition feedback
|
||||||
- Edit and save plugin-defined settings schemas from the same panel
|
- Edit and save plugin-defined settings schemas from the same panel
|
||||||
|
- Built-in runtime plugins (Hermes, Paperclip, OpenClaw, Droid) always expose an interactive enable/disable toggle in the Built-in Plugins list, even before install. Disabling one registers it and disables it in the same action, and the decision survives restarts — a disabled runtime is not re-activated on the next startup. The Runtimes settings cards mirror this state instead of showing a stale detected/connected status.
|
||||||
|
|
||||||
For full plugin lifecycle workflows (discovery, install, enable/disable, configure, update, uninstall, troubleshooting), see [Plugin Management](./plugin-management.md). For plugin-related settings and experimental toggles, see [Settings reference](./settings-reference.md).
|
For full plugin lifecycle workflows (discovery, install, enable/disable, configure, update, uninstall, troubleshooting), see [Plugin Management](./plugin-management.md). For plugin-related settings and experimental toggles, see [Settings reference](./settings-reference.md).
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ fn plugin disable <id>
|
|||||||
|
|
||||||
Expected outcome: Plugin is enabled or disabled by ID.
|
Expected outcome: Plugin is enabled or disabled by ID.
|
||||||
|
|
||||||
|
### Built-in runtime plugins (Hermes, Paperclip, OpenClaw, Droid)
|
||||||
|
|
||||||
|
Built-in runtime plugins always expose an interactive enable/disable toggle in **Built-in Plugins**, even before the plugin has been explicitly installed (no `plugin_installs` record yet). Disabling a not-yet-installed runtime registers it (mirroring the same lazy-install path used elsewhere) and then disables it in one step, so the decision persists. A user-disabled built-in runtime is never silently re-enabled or re-activated by Fusion on the next restart — startup auto-activation only loads plugins whose project state is enabled. The **Runtimes** settings cards (Hermes/OpenClaw/Paperclip) reflect this state and show "Disabled in Plugin Manager" instead of a stale detected/connected status when the runtime has been turned off.
|
||||||
|
|
||||||
## 5) Configure plugin settings
|
## 5) Configure plugin settings
|
||||||
|
|
||||||
1. Go to **Settings → Plugins → Fusion Plugins**.
|
1. Go to **Settings → Plugins → Fusion Plugins**.
|
||||||
|
|||||||
@@ -923,6 +923,52 @@ export default plugin;
|
|||||||
expect(loader.isPluginLoaded("disabled-plugin")).toBe(false);
|
expect(loader.isPluginLoaded("disabled-plugin")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:PluginLoader 2026-07-07-00:00:
|
||||||
|
* FN-7629 — regression coverage for the built-in runtime disable durability invariant.
|
||||||
|
* Symptom: with no plugin_installs row, a built-in runtime (e.g. Hermes) could not be
|
||||||
|
* disabled from the dashboard, and once registered+enabled it re-activated on every restart.
|
||||||
|
* The register-then-disable UI path (installPlugin + disablePlugin) relies on this store/loader
|
||||||
|
* contract: once a plugin's project state is enabled=false, loadAllPlugins must skip it on
|
||||||
|
* every subsequent load pass (i.e. every process restart) and must never record an activation
|
||||||
|
* event for it, even though the plugin_installs row itself persists untouched.
|
||||||
|
*/
|
||||||
|
it("keeps a user-disabled runtime built-in skipped and unactivated across repeated loadAllPlugins passes (restart simulation)", async () => {
|
||||||
|
await pluginStore.init();
|
||||||
|
|
||||||
|
const pluginDir = join(rootDir, "plugins");
|
||||||
|
const runtimePlugin = makePlugin(makeManifest({ id: "fusion-plugin-fake-runtime" }));
|
||||||
|
const runtimePath = await writePluginModule(pluginDir, "fake-runtime.js", runtimePlugin);
|
||||||
|
|
||||||
|
// Mirrors the UI's durable-disable path: register (defaults to enabled=true,
|
||||||
|
// same as installPlugin/registerPlugin), then immediately disable.
|
||||||
|
await pluginStore.registerPlugin({ manifest: runtimePlugin.manifest, path: runtimePath });
|
||||||
|
await pluginStore.disablePlugin("fusion-plugin-fake-runtime");
|
||||||
|
|
||||||
|
const firstLoader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||||
|
const firstResult = await firstLoader.loadAllPlugins();
|
||||||
|
|
||||||
|
expect(firstResult).toEqual({ loaded: 0, errors: 0 });
|
||||||
|
expect(firstLoader.isPluginLoaded("fusion-plugin-fake-runtime")).toBe(false);
|
||||||
|
expect(mockTaskStore.recordPluginActivation).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ pluginId: "fusion-plugin-fake-runtime" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate a second restart with a brand-new PluginLoader instance against the
|
||||||
|
// same persisted store — the disabled decision must still be honored.
|
||||||
|
mockTaskStore.recordPluginActivation.mockClear();
|
||||||
|
const secondLoader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||||
|
const secondResult = await secondLoader.loadAllPlugins();
|
||||||
|
|
||||||
|
expect(secondResult).toEqual({ loaded: 0, errors: 0 });
|
||||||
|
expect(secondLoader.isPluginLoaded("fusion-plugin-fake-runtime")).toBe(false);
|
||||||
|
expect(mockTaskStore.recordPluginActivation).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// The install row itself must still exist and remain disabled (durable, not deleted).
|
||||||
|
const stored = await pluginStore.getPlugin("fusion-plugin-fake-runtime");
|
||||||
|
expect(stored.enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns error count for failed plugins", async () => {
|
it("returns error count for failed plugins", async () => {
|
||||||
await pluginStore.init();
|
await pluginStore.init();
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
fetchHermesProfiles,
|
fetchHermesProfiles,
|
||||||
fetchHermesStatus,
|
fetchHermesStatus,
|
||||||
fetchPluginSettings,
|
fetchPluginSettings,
|
||||||
|
fetchPlugins,
|
||||||
updatePluginSettings,
|
updatePluginSettings,
|
||||||
type HermesProfileSummary,
|
type HermesProfileSummary,
|
||||||
type HermesProviderStatus,
|
type HermesProviderStatus,
|
||||||
@@ -72,6 +73,14 @@ export function HermesRuntimeCard() {
|
|||||||
const [profiles, setProfiles] = useState<HermesProfileSummary[]>([]);
|
const [profiles, setProfiles] = useState<HermesProfileSummary[]>([]);
|
||||||
const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | null>(null);
|
const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | null>(null);
|
||||||
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — Plugin Manager is the source of truth for the runtime's enable/disable decision.
|
||||||
|
* This card must not claim Hermes is active/detected when the user has disabled it there, so
|
||||||
|
* mirror the installed project-state (not the local settings form) and override the status
|
||||||
|
* badge when disabled.
|
||||||
|
*/
|
||||||
|
const [runtimeDisabled, setRuntimeDisabled] = useState(false);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -81,6 +90,20 @@ export function HermesRuntimeCard() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchPlugins()
|
||||||
|
.then((list) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const installed = list.find((p) => p.id === PLUGIN_ID);
|
||||||
|
setRuntimeDisabled(installed ? !installed.enabled : false);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBusy("loading");
|
setBusy("loading");
|
||||||
fetchPluginSettings(PLUGIN_ID)
|
fetchPluginSettings(PLUGIN_ID)
|
||||||
@@ -198,13 +221,16 @@ export function HermesRuntimeCard() {
|
|||||||
}, [buildPayload, probe, t]);
|
}, [buildPayload, probe, t]);
|
||||||
|
|
||||||
const binary = status?.binary;
|
const binary = status?.binary;
|
||||||
const statusKind = status === null
|
const statusKind = runtimeDisabled
|
||||||
? "loading"
|
? "neutral"
|
||||||
: binary?.available
|
: status === null
|
||||||
? "ok"
|
? "loading"
|
||||||
: "err";
|
: binary?.available
|
||||||
const statusText =
|
? "ok"
|
||||||
status === null
|
: "err";
|
||||||
|
const statusText = runtimeDisabled
|
||||||
|
? t("hermes.statusDisabledInPluginManager", "Disabled in Plugin Manager")
|
||||||
|
: status === null
|
||||||
? t("hermes.probing", "Probing local hermes binary…")
|
? t("hermes.probing", "Probing local hermes binary…")
|
||||||
: binary?.available
|
: binary?.available
|
||||||
? t("hermes.statusDetected", "✓ Detected{{version}}{{path}}", { version: binary.version ? ` ${binary.version}` : "", path: binary.binaryPath ? ` · ${binary.binaryPath}` : "" })
|
? t("hermes.statusDetected", "✓ Detected{{version}}{{path}}", { version: binary.version ? ` ${binary.version}` : "", path: binary.binaryPath ? ` · ${binary.binaryPath}` : "" })
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation, Trans } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
fetchOpenClawStatus,
|
fetchOpenClawStatus,
|
||||||
fetchPluginSettings,
|
fetchPluginSettings,
|
||||||
|
fetchPlugins,
|
||||||
updatePluginSettings,
|
updatePluginSettings,
|
||||||
type OpenClawProviderStatus,
|
type OpenClawProviderStatus,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
@@ -83,6 +84,13 @@ export function OpenClawRuntimeCard() {
|
|||||||
const [status, setStatus] = useState<OpenClawProviderStatus | null>(null);
|
const [status, setStatus] = useState<OpenClawProviderStatus | null>(null);
|
||||||
const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | null>(null);
|
const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | null>(null);
|
||||||
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — Plugin Manager is the source of truth for the runtime's enable/disable decision.
|
||||||
|
* Mirror the installed project-state so this card never claims OpenClaw is active/detected when
|
||||||
|
* the user has disabled it there.
|
||||||
|
*/
|
||||||
|
const [runtimeDisabled, setRuntimeDisabled] = useState(false);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,6 +100,20 @@ export function OpenClawRuntimeCard() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchPlugins()
|
||||||
|
.then((list) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const installed = list.find((p) => p.id === PLUGIN_ID);
|
||||||
|
setRuntimeDisabled(installed ? !installed.enabled : false);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load saved settings on mount.
|
// Load saved settings on mount.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBusy("loading");
|
setBusy("loading");
|
||||||
@@ -201,10 +223,12 @@ export function OpenClawRuntimeCard() {
|
|||||||
}, [buildPayload, probe, t]);
|
}, [buildPayload, probe, t]);
|
||||||
|
|
||||||
const binary = status?.binary;
|
const binary = status?.binary;
|
||||||
const statusKind =
|
const statusKind = runtimeDisabled
|
||||||
status === null ? "loading" : binary?.available ? "ok" : "err";
|
? "neutral"
|
||||||
const statusText =
|
: status === null ? "loading" : binary?.available ? "ok" : "err";
|
||||||
status === null
|
const statusText = runtimeDisabled
|
||||||
|
? t("openclaw.statusDisabledInPluginManager", "Disabled in Plugin Manager")
|
||||||
|
: status === null
|
||||||
? t("openclaw.probing", "Probing local openclaw binary…")
|
? t("openclaw.probing", "Probing local openclaw binary…")
|
||||||
: binary?.available
|
: binary?.available
|
||||||
? t("openclaw.statusDetected", `✓ Detected{{version}}{{path}}`, { version: binary.version ? ` ${binary.version}` : "", path: binary.binaryPath ? ` · ${binary.binaryPath}` : "" })
|
? t("openclaw.statusDetected", `✓ Detected{{version}}{{path}}`, { version: binary.version ? ` ${binary.version}` : "", path: binary.binaryPath ? ` · ${binary.binaryPath}` : "" })
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
fetchPaperclipCompanies,
|
fetchPaperclipCompanies,
|
||||||
fetchPaperclipStatus,
|
fetchPaperclipStatus,
|
||||||
fetchPluginSettings,
|
fetchPluginSettings,
|
||||||
|
fetchPlugins,
|
||||||
mintPaperclipApiKey,
|
mintPaperclipApiKey,
|
||||||
updatePluginSettings,
|
updatePluginSettings,
|
||||||
type PaperclipAgentSummary,
|
type PaperclipAgentSummary,
|
||||||
@@ -132,6 +133,13 @@ export function PaperclipRuntimeCard() {
|
|||||||
>(null);
|
>(null);
|
||||||
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
||||||
const [apiKeyDirty, setApiKeyDirty] = useState(false);
|
const [apiKeyDirty, setApiKeyDirty] = useState(false);
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — Plugin Manager is the source of truth for the runtime's enable/disable decision.
|
||||||
|
* Mirror the installed project-state so this card never claims Paperclip is connected/active
|
||||||
|
* when the user has disabled it there.
|
||||||
|
*/
|
||||||
|
const [runtimeDisabled, setRuntimeDisabled] = useState(false);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -141,6 +149,20 @@ export function PaperclipRuntimeCard() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchPlugins()
|
||||||
|
.then((list) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const installed = list.find((p) => p.id === PLUGIN_ID);
|
||||||
|
setRuntimeDisabled(installed ? !installed.enabled : false);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// The "effective" apiUrl/apiKey used for status + dropdowns.
|
// The "effective" apiUrl/apiKey used for status + dropdowns.
|
||||||
// In CLI mode we prefer whatever cliDiscovery returned, falling back to the
|
// In CLI mode we prefer whatever cliDiscovery returned, falling back to the
|
||||||
// typed apiUrl if discovery hasn't completed yet.
|
// typed apiUrl if discovery hasn't completed yet.
|
||||||
@@ -411,15 +433,17 @@ export function PaperclipRuntimeCard() {
|
|||||||
const identity = status?.connection.identity;
|
const identity = status?.connection.identity;
|
||||||
const cliOk = cliDiscovery?.ok === true;
|
const cliOk = cliDiscovery?.ok === true;
|
||||||
|
|
||||||
const statusKind =
|
const statusKind = runtimeDisabled
|
||||||
status === null
|
? "neutral"
|
||||||
|
: status === null
|
||||||
? "loading"
|
? "loading"
|
||||||
: connected
|
: connected
|
||||||
? "ok"
|
? "ok"
|
||||||
: "err";
|
: "err";
|
||||||
|
|
||||||
const statusText =
|
const statusText = runtimeDisabled
|
||||||
status === null
|
? t("paperclip.statusDisabledInPluginManager", "Disabled in Plugin Manager")
|
||||||
|
: status === null
|
||||||
? settings.transport === "cli" && cliDiscovery && !cliOk
|
? settings.transport === "cli" && cliDiscovery && !cliOk
|
||||||
? t("paperclip.statusCliDiscoveryFailed", "✗ CLI discovery failed: {{reason}}", { reason: cliDiscovery.reason })
|
? t("paperclip.statusCliDiscoveryFailed", "✗ CLI discovery failed: {{reason}}", { reason: cliDiscovery.reason })
|
||||||
: t("paperclip.statusProbing", "Probing Paperclip server…")
|
: t("paperclip.statusProbing", "Probing Paperclip server…")
|
||||||
|
|||||||
@@ -480,6 +480,20 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — runtime built-ins (Hermes/Paperclip/OpenClaw/Droid) must always expose a durable
|
||||||
|
* enable/disable control alongside the existing Install/Manage affordance, on both desktop and
|
||||||
|
* mobile, and never dead-end at a static label. Wrap the toggle + action button so both share the
|
||||||
|
* same alignment as the rest of the built-in row.
|
||||||
|
*/
|
||||||
|
.plugin-builtins-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-registry-section {
|
.plugin-registry-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -831,6 +845,26 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — keep the built-in runtime toggle at a 36x36 tap target on mobile, matching the
|
||||||
|
* installed-list `.plugin-actions .toggle-switch` convention, and let the actions row wrap
|
||||||
|
* below the metadata instead of squeezing the toggle + button onto one line.
|
||||||
|
*/
|
||||||
|
.plugin-builtins-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-builtins-actions .toggle-switch {
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-registry-header {
|
.plugin-registry-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -274,6 +274,14 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
const [builtinSetupStatusById, setBuiltinSetupStatusById] = useState<Record<string, PluginSetupStatusResponse>>({});
|
const [builtinSetupStatusById, setBuiltinSetupStatusById] = useState<Record<string, PluginSetupStatusResponse>>({});
|
||||||
const [loadingBuiltinSetupId, setLoadingBuiltinSetupId] = useState<string | null>(null);
|
const [loadingBuiltinSetupId, setLoadingBuiltinSetupId] = useState<string | null>(null);
|
||||||
const [installingBuiltinSetupId, setInstallingBuiltinSetupId] = useState<string | null>(null);
|
const [installingBuiltinSetupId, setInstallingBuiltinSetupId] = useState<string | null>(null);
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — built-in runtime plugins (Hermes/Paperclip/OpenClaw/Droid) must expose a durable
|
||||||
|
* enable/disable control even when no plugin_installs row exists yet ("activated-without-record").
|
||||||
|
* Track in-flight toggles separately from install/setup so the toggle-switch can show a busy
|
||||||
|
* state without blocking the Install/Manage button.
|
||||||
|
*/
|
||||||
|
const [togglingBuiltinRuntimeId, setTogglingBuiltinRuntimeId] = useState<string | null>(null);
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const loadPlugins = useCallback(async () => {
|
const loadPlugins = useCallback(async () => {
|
||||||
@@ -565,6 +573,48 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — durable built-in runtime disable path. A built-in runtime
|
||||||
|
* (Hermes/Paperclip/OpenClaw/Droid) that has never been explicitly installed
|
||||||
|
* has no plugin_installs row, so enablePlugin/disablePlugin (which both call
|
||||||
|
* getPlugin -> ENOENT) cannot persist a decision for it. When the user wants
|
||||||
|
* to disable such a runtime, first register it via the existing install path
|
||||||
|
* (mirrors the CLI's ensureBundledPluginInstalled lazy-install) so a
|
||||||
|
* plugin_installs row + project state exists, then disable it immediately so
|
||||||
|
* the decision is durable: loadAllPlugins() only loads plugins where
|
||||||
|
* enabled=true, so a disabled runtime is never re-activated on restart.
|
||||||
|
* Already-installed runtimes just toggle through the normal enable/disable
|
||||||
|
* handlers, same as the installed-plugin list row.
|
||||||
|
*/
|
||||||
|
const handleToggleBuiltinRuntime = async (builtinPlugin: BuiltinPlugin, installedPlugin?: PluginInstallation) => {
|
||||||
|
if (installedPlugin) {
|
||||||
|
if (installedPlugin.enabled) {
|
||||||
|
await handleDisable(installedPlugin);
|
||||||
|
} else {
|
||||||
|
await handleEnable(installedPlugin);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!builtinPlugin.path) {
|
||||||
|
addToast(t("plugins.builtinNoPackage", "{{name}} is built in and does not have an installable package yet", { name: builtinPlugin.name }), "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setTogglingBuiltinRuntimeId(builtinPlugin.id);
|
||||||
|
const registered = await installPlugin({ path: builtinPlugin.path }, projectId);
|
||||||
|
await disablePlugin(registered.id, projectId);
|
||||||
|
addToast(t("plugins.disabledForProject", "{{name}} disabled for this project", { name: builtinPlugin.name }), "success");
|
||||||
|
await loadPlugins();
|
||||||
|
} catch (err) {
|
||||||
|
addToast(t("plugins.disablePluginFailed", "Failed to disable plugin: {{error}}", { error: err instanceof Error ? err.message : String(err) }), "error");
|
||||||
|
} finally {
|
||||||
|
setTogglingBuiltinRuntimeId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleReload = async (plugin: PluginInstallation) => {
|
const handleReload = async (plugin: PluginInstallation) => {
|
||||||
try {
|
try {
|
||||||
setReloadingPluginId(plugin.id);
|
setReloadingPluginId(plugin.id);
|
||||||
@@ -1073,6 +1123,16 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
const setupReady = isInstalled && setupStatus?.hasSetup && pluginSetupState === "installed";
|
const setupReady = isInstalled && setupStatus?.hasSetup && pluginSetupState === "installed";
|
||||||
const setupCheckInFlight = loadingBuiltinSetupId === builtinPlugin.id;
|
const setupCheckInFlight = loadingBuiltinSetupId === builtinPlugin.id;
|
||||||
const metadataOnly = !builtinPlugin.path;
|
const metadataOnly = !builtinPlugin.path;
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — runtime built-ins (Hermes/Paperclip/OpenClaw/Droid) must always expose an
|
||||||
|
* interactive enable/disable control, independent of install status, and must never
|
||||||
|
* dead-end at the static "Built-in metadata only" label. Non-runtime built-ins (e.g.
|
||||||
|
* Agent Browser) keep the existing metadata-only/install/manage affordances unchanged.
|
||||||
|
*/
|
||||||
|
const isRuntimeBuiltin = builtinPlugin.category === "runtime";
|
||||||
|
const runtimeEnabled = installedPlugin ? installedPlugin.enabled : true;
|
||||||
|
const isTogglingRuntime = togglingBuiltinRuntimeId === builtinPlugin.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={builtinPlugin.id} className="plugin-builtins-item">
|
<div key={builtinPlugin.id} className="plugin-builtins-item">
|
||||||
@@ -1083,6 +1143,9 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
<span className={`plugin-builtins-status ${isInstalled ? "plugin-builtins-status--installed" : "plugin-builtins-status--available"}`}>
|
<span className={`plugin-builtins-status ${isInstalled ? "plugin-builtins-status--installed" : "plugin-builtins-status--available"}`}>
|
||||||
{isInstalled ? t("plugins.statusInstalled", "Installed") : metadataOnly ? t("plugins.statusBuiltIn", "Built in") : t("plugins.statusNotInstalled", "Not installed")}
|
{isInstalled ? t("plugins.statusInstalled", "Installed") : metadataOnly ? t("plugins.statusBuiltIn", "Built in") : t("plugins.statusNotInstalled", "Not installed")}
|
||||||
</span>
|
</span>
|
||||||
|
{isRuntimeBuiltin && !runtimeEnabled && (
|
||||||
|
<span className="plugin-builtins-setup-status plugin-builtins-setup-status--warning">{t("plugins.builtinDisabled", "Disabled")}</span>
|
||||||
|
)}
|
||||||
{requiresSetupAction && (
|
{requiresSetupAction && (
|
||||||
<span className="plugin-builtins-setup-status plugin-builtins-setup-status--warning">{t("plugins.setupRequired", "Setup required")}</span>
|
<span className="plugin-builtins-setup-status plugin-builtins-setup-status--warning">{t("plugins.setupRequired", "Setup required")}</span>
|
||||||
)}
|
)}
|
||||||
@@ -1097,53 +1160,69 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
|||||||
)}
|
)}
|
||||||
<span className="plugin-builtins-description-text">{builtinPlugin.description}</span>
|
<span className="plugin-builtins-description-text">{builtinPlugin.description}</span>
|
||||||
</div>
|
</div>
|
||||||
{metadataOnly ? (
|
<div className="plugin-builtins-actions">
|
||||||
isInstalled && requiresSetupAction ? (
|
{isRuntimeBuiltin && (
|
||||||
<button
|
<label className="toggle-switch">
|
||||||
className="btn btn-primary btn-sm"
|
<input
|
||||||
onClick={() => void handleInstallBuiltinSetup(builtinPlugin)}
|
type="checkbox"
|
||||||
disabled={installingBuiltinSetupId === builtinPlugin.id || setupCheckInFlight}
|
checked={runtimeEnabled}
|
||||||
>
|
onChange={() => void handleToggleBuiltinRuntime(builtinPlugin, installedPlugin)}
|
||||||
{installingBuiltinSetupId === builtinPlugin.id ? t("plugins.settingUp", "Setting up...") : t("plugins.installSetup", "Install Setup")}
|
disabled={isTogglingRuntime}
|
||||||
</button>
|
aria-label={runtimeEnabled
|
||||||
) : isInstalled && installedPlugin ? (
|
? t("plugins.disablePlugin", "Disable {{name}}", { name: builtinPlugin.name })
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => void handleSelectPlugin(installedPlugin)}>
|
: t("plugins.enablePlugin", "Enable {{name}}", { name: builtinPlugin.name })}
|
||||||
{t("plugins.manage", "Manage")}
|
/>
|
||||||
</button>
|
<span className="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{metadataOnly ? (
|
||||||
|
isInstalled && requiresSetupAction ? (
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={() => void handleInstallBuiltinSetup(builtinPlugin)}
|
||||||
|
disabled={installingBuiltinSetupId === builtinPlugin.id || setupCheckInFlight}
|
||||||
|
>
|
||||||
|
{installingBuiltinSetupId === builtinPlugin.id ? t("plugins.settingUp", "Setting up...") : t("plugins.installSetup", "Install Setup")}
|
||||||
|
</button>
|
||||||
|
) : isInstalled && installedPlugin ? (
|
||||||
|
<button className="btn btn-secondary btn-sm" onClick={() => void handleSelectPlugin(installedPlugin)}>
|
||||||
|
{t("plugins.manage", "Manage")}
|
||||||
|
</button>
|
||||||
|
) : isRuntimeBuiltin ? null : (
|
||||||
|
<span className="plugin-builtins-metadata-only">{t("plugins.builtinMetadataOnly", "Built-in metadata only")}</span>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<span className="plugin-builtins-metadata-only">{t("plugins.builtinMetadataOnly", "Built-in metadata only")}</span>
|
<button
|
||||||
)
|
className={`btn ${(isInstalled && !requiresSetupAction) ? "btn-secondary" : "btn-primary"} btn-sm`}
|
||||||
) : (
|
onClick={() => {
|
||||||
<button
|
if (!isInstalled) {
|
||||||
className={`btn ${(isInstalled && !requiresSetupAction) ? "btn-secondary" : "btn-primary"} btn-sm`}
|
void handleInstallBuiltinPlugin(builtinPlugin);
|
||||||
onClick={() => {
|
return;
|
||||||
if (!isInstalled) {
|
}
|
||||||
void handleInstallBuiltinPlugin(builtinPlugin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresSetupAction) {
|
if (requiresSetupAction) {
|
||||||
void handleInstallBuiltinSetup(builtinPlugin);
|
void handleInstallBuiltinSetup(builtinPlugin);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installedPlugin) {
|
if (installedPlugin) {
|
||||||
void handleSelectPlugin(installedPlugin);
|
void handleSelectPlugin(installedPlugin);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
installingBuiltinPluginId === builtinPlugin.id
|
||||||
|
|| installingBuiltinSetupId === builtinPlugin.id
|
||||||
|
|| setupCheckInFlight
|
||||||
}
|
}
|
||||||
}}
|
>
|
||||||
disabled={
|
{!isInstalled
|
||||||
installingBuiltinPluginId === builtinPlugin.id
|
? (installingBuiltinPluginId === builtinPlugin.id ? t("plugins.installing", "Installing...") : t("plugins.installNamed", "Install {{name}}", { name: builtinPlugin.name }))
|
||||||
|| installingBuiltinSetupId === builtinPlugin.id
|
: requiresSetupAction
|
||||||
|| setupCheckInFlight
|
? (installingBuiltinSetupId === builtinPlugin.id ? t("plugins.settingUp", "Setting up...") : t("plugins.installSetup", "Install Setup"))
|
||||||
}
|
: t("plugins.manage", "Manage")}
|
||||||
>
|
</button>
|
||||||
{!isInstalled
|
)}
|
||||||
? (installingBuiltinPluginId === builtinPlugin.id ? t("plugins.installing", "Installing...") : t("plugins.installNamed", "Install {{name}}", { name: builtinPlugin.name }))
|
</div>
|
||||||
: requiresSetupAction
|
|
||||||
? (installingBuiltinSetupId === builtinPlugin.id ? t("plugins.settingUp", "Setting up...") : t("plugins.installSetup", "Install Setup"))
|
|
||||||
: t("plugins.manage", "Manage")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -767,7 +767,7 @@ describe("PluginManager", () => {
|
|||||||
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = screen.getByRole("checkbox");
|
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||||
expect(toggle).toBeTruthy();
|
expect(toggle).toBeTruthy();
|
||||||
expect(toggle).not.toBeChecked();
|
expect(toggle).not.toBeChecked();
|
||||||
|
|
||||||
@@ -796,7 +796,7 @@ describe("PluginManager", () => {
|
|||||||
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("checkbox"));
|
await userEvent.click(screen.getByRole("checkbox", { name: /Test Plugin A/ }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(addToast).toHaveBeenCalledWith(
|
expect(addToast).toHaveBeenCalledWith(
|
||||||
@@ -818,7 +818,7 @@ describe("PluginManager", () => {
|
|||||||
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("checkbox"));
|
await userEvent.click(screen.getByRole("checkbox", { name: /Test Plugin A/ }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(addToast).toHaveBeenCalledWith("Failed to enable plugin: network", "error");
|
expect(addToast).toHaveBeenCalledWith("Failed to enable plugin: network", "error");
|
||||||
@@ -835,7 +835,7 @@ describe("PluginManager", () => {
|
|||||||
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = screen.getByRole("checkbox");
|
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||||
expect(toggle).toBeTruthy();
|
expect(toggle).toBeTruthy();
|
||||||
expect(toggle).toBeChecked();
|
expect(toggle).toBeChecked();
|
||||||
|
|
||||||
@@ -1150,7 +1150,7 @@ describe("PluginManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const toggle = screen.getByRole("checkbox");
|
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||||
expect(toggle).toBeChecked();
|
expect(toggle).toBeChecked();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1183,7 +1183,7 @@ describe("PluginManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const toggle = screen.getByRole("checkbox");
|
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||||
expect(toggle).not.toBeChecked();
|
expect(toggle).not.toBeChecked();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { render, screen, waitFor, cleanup } from "@testing-library/react";
|
import { render, screen, waitFor, cleanup, within } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { PluginManager } from "../PluginManager";
|
import { PluginManager } from "../PluginManager";
|
||||||
import { loadAllAppCss } from "../../test/cssFixture";
|
import { loadAllAppCss } from "../../test/cssFixture";
|
||||||
@@ -21,7 +21,8 @@ vi.mock("../../api", () => ({
|
|||||||
browseDirectory: vi.fn(() => Promise.resolve({ currentPath: "/", parentPath: null, entries: [] })),
|
browseDirectory: vi.fn(() => Promise.resolve({ currentPath: "/", parentPath: null, entries: [] })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { fetchPlugins, disablePlugin } from "../../api";
|
import { fetchPlugins, disablePlugin, enablePlugin, installPlugin } from "../../api";
|
||||||
|
import { BUILTIN_PLUGINS } from "../PluginManager";
|
||||||
|
|
||||||
const addToast = vi.fn();
|
const addToast = vi.fn();
|
||||||
|
|
||||||
@@ -71,6 +72,22 @@ afterEach(() => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function builtinPlugin(id: string, enabled: boolean) {
|
||||||
|
const builtin = BUILTIN_PLUGINS.find((p) => p.id === id)!;
|
||||||
|
return {
|
||||||
|
id: builtin.id,
|
||||||
|
name: builtin.name,
|
||||||
|
version: "1.0.0",
|
||||||
|
state: "started" as const,
|
||||||
|
enabled,
|
||||||
|
path: builtin.path ?? "/plugins/unknown",
|
||||||
|
settings: {},
|
||||||
|
settingsSchema: {},
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("PluginManager toggle switch", () => {
|
describe("PluginManager toggle switch", () => {
|
||||||
it("keeps checkbox focusable but visually hidden", async () => {
|
it("keeps checkbox focusable but visually hidden", async () => {
|
||||||
vi.mocked(fetchPlugins).mockResolvedValue([plugin(true)]);
|
vi.mocked(fetchPlugins).mockResolvedValue([plugin(true)]);
|
||||||
@@ -117,3 +134,86 @@ describe("PluginManager toggle switch", () => {
|
|||||||
expect(disabled.nextElementSibling).toHaveClass("toggle-slider");
|
expect(disabled.nextElementSibling).toHaveClass("toggle-slider");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:PluginManager 2026-07-07-00:00:
|
||||||
|
* FN-7629 — regression coverage for the durable built-in runtime enable/disable control.
|
||||||
|
* Symptom: with no plugin_installs row for Hermes/Paperclip/OpenClaw/Droid (fetchPlugins -> []),
|
||||||
|
* the built-in runtime rows dead-ended at a static "Built-in metadata only"/install-only CTA and
|
||||||
|
* offered no way to disable the runtime. Assert every runtime built-in exposes an interactive
|
||||||
|
* toggle in every data state, and clicking disable on a not-installed runtime persists via
|
||||||
|
* installPlugin + disablePlugin (the durable register-then-disable path).
|
||||||
|
*/
|
||||||
|
describe("PluginManager built-in runtime enable/disable toggle (FN-7629)", () => {
|
||||||
|
const RUNTIME_BUILTINS = BUILTIN_PLUGINS.filter((p) => p.category === "runtime");
|
||||||
|
|
||||||
|
async function builtinSection() {
|
||||||
|
return within(await screen.findByLabelText("Built-in plugin recommendations"));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders an interactive, checked toggle for every runtime built-in when not installed", async () => {
|
||||||
|
vi.mocked(fetchPlugins).mockResolvedValue([]);
|
||||||
|
|
||||||
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
const section = await builtinSection();
|
||||||
|
|
||||||
|
for (const runtime of RUNTIME_BUILTINS) {
|
||||||
|
const toggle = await section.findByRole("checkbox", { name: `Disable ${runtime.name}` });
|
||||||
|
expect(toggle).toBeChecked();
|
||||||
|
expect(toggle.closest("label.toggle-switch")).not.toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No dead-end "Built-in metadata only" label for any runtime built-in row
|
||||||
|
// (the metadata-only dead-end may still legitimately appear for non-runtime
|
||||||
|
// built-ins like Agent Browser, which this task does not change).
|
||||||
|
for (const runtime of RUNTIME_BUILTINS) {
|
||||||
|
const row = section.getByText(runtime.name).closest(".plugin-builtins-item") as HTMLElement;
|
||||||
|
expect(within(row).queryByText("Built-in metadata only")).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disabling a not-installed runtime built-in registers it then disables it (durable path)", async () => {
|
||||||
|
vi.mocked(fetchPlugins).mockResolvedValue([]);
|
||||||
|
vi.mocked(installPlugin).mockResolvedValue(builtinPlugin("fusion-plugin-hermes-runtime", true));
|
||||||
|
|
||||||
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
|
||||||
|
const toggle = await (await builtinSection()).findByRole("checkbox", { name: "Disable Hermes Runtime" });
|
||||||
|
await userEvent.click(toggle.closest("label.toggle-switch") as HTMLLabelElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(installPlugin).toHaveBeenCalledWith({ path: "./plugins/fusion-plugin-hermes-runtime" }, undefined);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(disablePlugin).toHaveBeenCalledWith("fusion-plugin-hermes-runtime", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles an installed, enabled runtime built-in via the standard disable path (no re-install)", async () => {
|
||||||
|
vi.mocked(fetchPlugins).mockResolvedValue([builtinPlugin("fusion-plugin-paperclip-runtime", true)]);
|
||||||
|
|
||||||
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
|
||||||
|
const toggle = await (await builtinSection()).findByRole("checkbox", { name: "Disable Paperclip Runtime" });
|
||||||
|
await userEvent.click(toggle.closest("label.toggle-switch") as HTMLLabelElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(disablePlugin).toHaveBeenCalledWith("fusion-plugin-paperclip-runtime", undefined);
|
||||||
|
});
|
||||||
|
expect(installPlugin).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles an installed, disabled runtime built-in back on via the standard enable path", async () => {
|
||||||
|
vi.mocked(fetchPlugins).mockResolvedValue([builtinPlugin("fusion-plugin-openclaw-runtime", false)]);
|
||||||
|
|
||||||
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
|
||||||
|
const toggle = await (await builtinSection()).findByRole("checkbox", { name: "Enable OpenClaw Runtime" });
|
||||||
|
expect(toggle).not.toBeChecked();
|
||||||
|
await userEvent.click(toggle.closest("label.toggle-switch") as HTMLLabelElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(enablePlugin).toHaveBeenCalledWith("fusion-plugin-openclaw-runtime", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3039,6 +3039,7 @@
|
|||||||
"savedProbeFailed": "Saved, but probe failed.",
|
"savedProbeFailed": "Saved, but probe failed.",
|
||||||
"settingsSaved": "Settings saved.",
|
"settingsSaved": "Settings saved.",
|
||||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "by Nous Research",
|
"subname": "by Nous Research",
|
||||||
"testFailed": "Test failed — see status above.",
|
"testFailed": "Test failed — see status above.",
|
||||||
@@ -4401,6 +4402,7 @@
|
|||||||
"savedNotFound": "Saved · ✗ openclaw not found",
|
"savedNotFound": "Saved · ✗ openclaw not found",
|
||||||
"savedProbeFailed": "Saved, but probe failed.",
|
"savedProbeFailed": "Saved, but probe failed.",
|
||||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||||
"statusNotFound": "✗ not detected on PATH",
|
"statusNotFound": "✗ not detected on PATH",
|
||||||
"subprocess": "CLI subprocess timeout (ms)",
|
"subprocess": "CLI subprocess timeout (ms)",
|
||||||
"subprocessHint": "Fusion-side hard cap before the CLI subprocess is killed (default: {{default}}s).",
|
"subprocessHint": "Fusion-side hard cap before the CLI subprocess is killed (default: {{default}}s).",
|
||||||
@@ -4464,6 +4466,7 @@
|
|||||||
"statusCliDiscoveryFailed": "CLI discovery failed: {{reason}}",
|
"statusCliDiscoveryFailed": "CLI discovery failed: {{reason}}",
|
||||||
"statusConnected": "Connected",
|
"statusConnected": "Connected",
|
||||||
"statusConnectedAs": "Connected as {{agentName}}{{roleInfo}}{{companyInfo}}",
|
"statusConnectedAs": "Connected as {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||||
|
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||||
"statusProbing": "Probing Paperclip server…",
|
"statusProbing": "Probing Paperclip server…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API (URL + token)",
|
"tabApi": "API (URL + token)",
|
||||||
@@ -4660,6 +4663,7 @@
|
|||||||
"author": "Author:",
|
"author": "Author:",
|
||||||
"backToList": "Back to plugin list",
|
"backToList": "Back to plugin list",
|
||||||
"browseRegistry": "Browse Registry",
|
"browseRegistry": "Browse Registry",
|
||||||
|
"builtinDisabled": "Disabled",
|
||||||
"builtinInstalledGlobally": "{{name}} installed globally",
|
"builtinInstalledGlobally": "{{name}} installed globally",
|
||||||
"builtinInstallFailed": "Failed to install {{name}}: {{error}}",
|
"builtinInstallFailed": "Failed to install {{name}}: {{error}}",
|
||||||
"builtinMetadataOnly": "Built-in metadata only",
|
"builtinMetadataOnly": "Built-in metadata only",
|
||||||
|
|||||||
@@ -3029,6 +3029,7 @@
|
|||||||
"savedProbeFailed": "Guardado, pero la prueba falló.",
|
"savedProbeFailed": "Guardado, pero la prueba falló.",
|
||||||
"settingsSaved": "Configuración guardada.",
|
"settingsSaved": "Configuración guardada.",
|
||||||
"statusDetected": "✓ Detectado{{version}}{{path}}",
|
"statusDetected": "✓ Detectado{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Deshabilitado en el gestor de complementos",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "por Nous Research",
|
"subname": "por Nous Research",
|
||||||
"testFailed": "La prueba falló — consulte el estado anterior.",
|
"testFailed": "La prueba falló — consulte el estado anterior.",
|
||||||
@@ -4391,6 +4392,7 @@
|
|||||||
"savedNotFound": "Guardado · ✗ openclaw no encontrado",
|
"savedNotFound": "Guardado · ✗ openclaw no encontrado",
|
||||||
"savedProbeFailed": "Guardado, pero la prueba falló.",
|
"savedProbeFailed": "Guardado, pero la prueba falló.",
|
||||||
"statusDetected": "✓ Detectado{{version}}{{path}}",
|
"statusDetected": "✓ Detectado{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Deshabilitado en el gestor de complementos",
|
||||||
"statusNotFound": "✗ no detectado en PATH",
|
"statusNotFound": "✗ no detectado en PATH",
|
||||||
"subprocess": "Tiempo de espera del subproceso CLI (ms)",
|
"subprocess": "Tiempo de espera del subproceso CLI (ms)",
|
||||||
"subprocessHint": "Límite de Fusion antes de que se mate el subproceso CLI (predeterminado: {{default}}s).",
|
"subprocessHint": "Límite de Fusion antes de que se mate el subproceso CLI (predeterminado: {{default}}s).",
|
||||||
@@ -4454,6 +4456,7 @@
|
|||||||
"statusCliDiscoveryFailed": "Error al descubrir CLI: {{reason}}",
|
"statusCliDiscoveryFailed": "Error al descubrir CLI: {{reason}}",
|
||||||
"statusConnected": "Conectado",
|
"statusConnected": "Conectado",
|
||||||
"statusConnectedAs": "Conectado como {{agentName}}{{roleInfo}}{{companyInfo}}",
|
"statusConnectedAs": "Conectado como {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||||
|
"statusDisabledInPluginManager": "Deshabilitado en el gestor de complementos",
|
||||||
"statusProbing": "Sondeando servidor de Paperclip…",
|
"statusProbing": "Sondeando servidor de Paperclip…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API (URL + token)",
|
"tabApi": "API (URL + token)",
|
||||||
@@ -4650,6 +4653,7 @@
|
|||||||
"author": "Autor:",
|
"author": "Autor:",
|
||||||
"backToList": "Volver a la lista de plugins",
|
"backToList": "Volver a la lista de plugins",
|
||||||
"browseRegistry": "",
|
"browseRegistry": "",
|
||||||
|
"builtinDisabled": "Deshabilitado",
|
||||||
"builtinInstalledGlobally": "{{name}} instalado globalmente",
|
"builtinInstalledGlobally": "{{name}} instalado globalmente",
|
||||||
"builtinInstallFailed": "Error al instalar {{name}}: {{error}}",
|
"builtinInstallFailed": "Error al instalar {{name}}: {{error}}",
|
||||||
"builtinMetadataOnly": "Solo metadatos integrados",
|
"builtinMetadataOnly": "Solo metadatos integrados",
|
||||||
|
|||||||
@@ -3029,6 +3029,7 @@
|
|||||||
"savedProbeFailed": "Enregistré, mais la sonde a échoué.",
|
"savedProbeFailed": "Enregistré, mais la sonde a échoué.",
|
||||||
"settingsSaved": "Paramètres enregistrés.",
|
"settingsSaved": "Paramètres enregistrés.",
|
||||||
"statusDetected": "✓ Détecté{{version}}{{path}}",
|
"statusDetected": "✓ Détecté{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Désactivé dans le gestionnaire de plugins",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "par Nous Research",
|
"subname": "par Nous Research",
|
||||||
"testFailed": "Le test a échoué — voir le statut ci-dessus.",
|
"testFailed": "Le test a échoué — voir le statut ci-dessus.",
|
||||||
@@ -4391,6 +4392,7 @@
|
|||||||
"savedNotFound": "Enregistré · ✗ openclaw non trouvé",
|
"savedNotFound": "Enregistré · ✗ openclaw non trouvé",
|
||||||
"savedProbeFailed": "Enregistré, mais la sonde a échoué.",
|
"savedProbeFailed": "Enregistré, mais la sonde a échoué.",
|
||||||
"statusDetected": "✓ Détecté{{version}}{{path}}",
|
"statusDetected": "✓ Détecté{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Désactivé dans le gestionnaire de plugins",
|
||||||
"statusNotFound": "✗ non détecté sur PATH",
|
"statusNotFound": "✗ non détecté sur PATH",
|
||||||
"subprocess": "Délai d'expiration du sous-processus CLI (ms)",
|
"subprocess": "Délai d'expiration du sous-processus CLI (ms)",
|
||||||
"subprocessHint": "Limite côté Fusion avant la suppression du sous-processus CLI (par défaut: {{default}}s).",
|
"subprocessHint": "Limite côté Fusion avant la suppression du sous-processus CLI (par défaut: {{default}}s).",
|
||||||
@@ -4454,6 +4456,7 @@
|
|||||||
"statusCliDiscoveryFailed": "Échec de la découverte CLI : {{reason}}",
|
"statusCliDiscoveryFailed": "Échec de la découverte CLI : {{reason}}",
|
||||||
"statusConnected": "Connecté",
|
"statusConnected": "Connecté",
|
||||||
"statusConnectedAs": "Connecté en tant que {{agentName}}{{roleInfo}}{{companyInfo}}",
|
"statusConnectedAs": "Connecté en tant que {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||||
|
"statusDisabledInPluginManager": "Désactivé dans le gestionnaire de plugins",
|
||||||
"statusProbing": "Sondage du serveur Paperclip…",
|
"statusProbing": "Sondage du serveur Paperclip…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API (URL + jeton)",
|
"tabApi": "API (URL + jeton)",
|
||||||
@@ -4650,6 +4653,7 @@
|
|||||||
"author": "Auteur :",
|
"author": "Auteur :",
|
||||||
"backToList": "Retour à la liste des plugins",
|
"backToList": "Retour à la liste des plugins",
|
||||||
"browseRegistry": "",
|
"browseRegistry": "",
|
||||||
|
"builtinDisabled": "Désactivé",
|
||||||
"builtinInstalledGlobally": "{{name}} installé globalement",
|
"builtinInstalledGlobally": "{{name}} installé globalement",
|
||||||
"builtinInstallFailed": "Échec de l'installation de {{name}} : {{error}}",
|
"builtinInstallFailed": "Échec de l'installation de {{name}} : {{error}}",
|
||||||
"builtinMetadataOnly": "Métadonnées intégrées uniquement",
|
"builtinMetadataOnly": "Métadonnées intégrées uniquement",
|
||||||
|
|||||||
@@ -3029,6 +3029,7 @@
|
|||||||
"savedProbeFailed": "저장됨, 하지만 탐색에 실패했습니다.",
|
"savedProbeFailed": "저장됨, 하지만 탐색에 실패했습니다.",
|
||||||
"settingsSaved": "설정이 저장되었습니다.",
|
"settingsSaved": "설정이 저장되었습니다.",
|
||||||
"statusDetected": "✓ 감지됨{{version}}{{path}}",
|
"statusDetected": "✓ 감지됨{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "플러그인 관리자에서 비활성화됨",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "by Nous Research",
|
"subname": "by Nous Research",
|
||||||
"testFailed": "테스트 실패 — 위의 상태를 확인하세요.",
|
"testFailed": "테스트 실패 — 위의 상태를 확인하세요.",
|
||||||
@@ -4391,6 +4392,7 @@
|
|||||||
"savedNotFound": "저장됨 · ✗ openclaw를 찾을 수 없음",
|
"savedNotFound": "저장됨 · ✗ openclaw를 찾을 수 없음",
|
||||||
"savedProbeFailed": "저장되었지만, 확인에 실패했습니다.",
|
"savedProbeFailed": "저장되었지만, 확인에 실패했습니다.",
|
||||||
"statusDetected": "✓ 감지됨{{version}}{{path}}",
|
"statusDetected": "✓ 감지됨{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "플러그인 관리자에서 비활성화됨",
|
||||||
"statusNotFound": "✗ PATH에서 감지되지 않음",
|
"statusNotFound": "✗ PATH에서 감지되지 않음",
|
||||||
"subprocess": "CLI 서브프로세스 타임아웃 (ms)",
|
"subprocess": "CLI 서브프로세스 타임아웃 (ms)",
|
||||||
"subprocessHint": "CLI 서브프로세스가 종료되기 전 Fusion 측 최대 대기 시간 (기본값: {{default}}s).",
|
"subprocessHint": "CLI 서브프로세스가 종료되기 전 Fusion 측 최대 대기 시간 (기본값: {{default}}s).",
|
||||||
@@ -4454,6 +4456,7 @@
|
|||||||
"statusCliDiscoveryFailed": "CLI 검색에 실패했습니다: {{reason}}",
|
"statusCliDiscoveryFailed": "CLI 검색에 실패했습니다: {{reason}}",
|
||||||
"statusConnected": "연결됨",
|
"statusConnected": "연결됨",
|
||||||
"statusConnectedAs": "{{agentName}}{{roleInfo}}{{companyInfo}}(으)로 연결됨",
|
"statusConnectedAs": "{{agentName}}{{roleInfo}}{{companyInfo}}(으)로 연결됨",
|
||||||
|
"statusDisabledInPluginManager": "플러그인 관리자에서 비활성화됨",
|
||||||
"statusProbing": "Paperclip 서버 확인 중…",
|
"statusProbing": "Paperclip 서버 확인 중…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API (URL + 토큰)",
|
"tabApi": "API (URL + 토큰)",
|
||||||
@@ -4650,6 +4653,7 @@
|
|||||||
"author": "작성자:",
|
"author": "작성자:",
|
||||||
"backToList": "플러그인 목록으로 돌아가기",
|
"backToList": "플러그인 목록으로 돌아가기",
|
||||||
"browseRegistry": "",
|
"browseRegistry": "",
|
||||||
|
"builtinDisabled": "비활성화됨",
|
||||||
"builtinInstalledGlobally": "{{name}} 전역 설치됨",
|
"builtinInstalledGlobally": "{{name}} 전역 설치됨",
|
||||||
"builtinInstallFailed": "{{name}} 설치 실패: {{error}}",
|
"builtinInstallFailed": "{{name}} 설치 실패: {{error}}",
|
||||||
"builtinMetadataOnly": "기본 제공 메타데이터만",
|
"builtinMetadataOnly": "기본 제공 메타데이터만",
|
||||||
|
|||||||
@@ -3029,6 +3029,7 @@
|
|||||||
"savedProbeFailed": "已保存,但探测失败。",
|
"savedProbeFailed": "已保存,但探测失败。",
|
||||||
"settingsSaved": "设置已保存。",
|
"settingsSaved": "设置已保存。",
|
||||||
"statusDetected": "✓ 已检测{{version}}{{path}}",
|
"statusDetected": "✓ 已检测{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "已在插件管理器中禁用",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "由 Nous Research 提供",
|
"subname": "由 Nous Research 提供",
|
||||||
"testFailed": "测试失败 — 见上方状态。",
|
"testFailed": "测试失败 — 见上方状态。",
|
||||||
@@ -4391,6 +4392,7 @@
|
|||||||
"savedNotFound": "已保存 · ✗ openclaw 未找到",
|
"savedNotFound": "已保存 · ✗ openclaw 未找到",
|
||||||
"savedProbeFailed": "已保存,但探测失败。",
|
"savedProbeFailed": "已保存,但探测失败。",
|
||||||
"statusDetected": "✓ 已检测到{{version}}{{path}}",
|
"statusDetected": "✓ 已检测到{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "已在插件管理器中禁用",
|
||||||
"statusNotFound": "✗ 在 PATH 上未检测到",
|
"statusNotFound": "✗ 在 PATH 上未检测到",
|
||||||
"subprocess": "CLI 子进程超时(毫秒)",
|
"subprocess": "CLI 子进程超时(毫秒)",
|
||||||
"subprocessHint": "Fusion 端限制,超时后 CLI 子进程被终止(默认:{{default}}s)。",
|
"subprocessHint": "Fusion 端限制,超时后 CLI 子进程被终止(默认:{{default}}s)。",
|
||||||
@@ -4454,6 +4456,7 @@
|
|||||||
"statusCliDiscoveryFailed": "CLI 发现失败:{{reason}}",
|
"statusCliDiscoveryFailed": "CLI 发现失败:{{reason}}",
|
||||||
"statusConnected": "已连接",
|
"statusConnected": "已连接",
|
||||||
"statusConnectedAs": "已连接为 {{agentName}}{{roleInfo}}{{companyInfo}}",
|
"statusConnectedAs": "已连接为 {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||||
|
"statusDisabledInPluginManager": "已在插件管理器中禁用",
|
||||||
"statusProbing": "正在探测 Paperclip 服务器…",
|
"statusProbing": "正在探测 Paperclip 服务器…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API(URL + 令牌)",
|
"tabApi": "API(URL + 令牌)",
|
||||||
@@ -4650,6 +4653,7 @@
|
|||||||
"author": "作者:",
|
"author": "作者:",
|
||||||
"backToList": "返回插件列表",
|
"backToList": "返回插件列表",
|
||||||
"browseRegistry": "",
|
"browseRegistry": "",
|
||||||
|
"builtinDisabled": "已禁用",
|
||||||
"builtinInstalledGlobally": "{{name}} 已全局安装",
|
"builtinInstalledGlobally": "{{name}} 已全局安装",
|
||||||
"builtinInstallFailed": "安装 {{name}} 失败:{{error}}",
|
"builtinInstallFailed": "安装 {{name}} 失败:{{error}}",
|
||||||
"builtinMetadataOnly": "仅内置元数据",
|
"builtinMetadataOnly": "仅内置元数据",
|
||||||
|
|||||||
@@ -3029,6 +3029,7 @@
|
|||||||
"savedProbeFailed": "已保存,但探測失敗。",
|
"savedProbeFailed": "已保存,但探測失敗。",
|
||||||
"settingsSaved": "設置已保存。",
|
"settingsSaved": "設置已保存。",
|
||||||
"statusDetected": "✓ 已偵測{{version}}{{path}}",
|
"statusDetected": "✓ 已偵測{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "已在外掛程式管理器中停用",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "由 Nous Research 提供",
|
"subname": "由 Nous Research 提供",
|
||||||
"testFailed": "測試失敗 — 見上方狀態。",
|
"testFailed": "測試失敗 — 見上方狀態。",
|
||||||
@@ -4391,6 +4392,7 @@
|
|||||||
"savedNotFound": "已保存 · ✗ openclaw 未找到",
|
"savedNotFound": "已保存 · ✗ openclaw 未找到",
|
||||||
"savedProbeFailed": "已保存,但探測失敗。",
|
"savedProbeFailed": "已保存,但探測失敗。",
|
||||||
"statusDetected": "✓ 已偵測到{{version}}{{path}}",
|
"statusDetected": "✓ 已偵測到{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "已在外掛程式管理器中停用",
|
||||||
"statusNotFound": "✗ 在 PATH 上未偵測到",
|
"statusNotFound": "✗ 在 PATH 上未偵測到",
|
||||||
"subprocess": "CLI 子進程逾時(毫秒)",
|
"subprocess": "CLI 子進程逾時(毫秒)",
|
||||||
"subprocessHint": "Fusion 端限制,逾時後 CLI 子進程被終止(預設:{{default}}s)。",
|
"subprocessHint": "Fusion 端限制,逾時後 CLI 子進程被終止(預設:{{default}}s)。",
|
||||||
@@ -4454,6 +4456,7 @@
|
|||||||
"statusCliDiscoveryFailed": "CLI 發現失敗:{{reason}}",
|
"statusCliDiscoveryFailed": "CLI 發現失敗:{{reason}}",
|
||||||
"statusConnected": "已連線",
|
"statusConnected": "已連線",
|
||||||
"statusConnectedAs": "已連線為 {{agentName}}{{roleInfo}}{{companyInfo}}",
|
"statusConnectedAs": "已連線為 {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||||
|
"statusDisabledInPluginManager": "已在外掛程式管理器中停用",
|
||||||
"statusProbing": "正在探測 Paperclip 伺服器…",
|
"statusProbing": "正在探測 Paperclip 伺服器…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API(URL + 令牌)",
|
"tabApi": "API(URL + 令牌)",
|
||||||
@@ -4650,6 +4653,7 @@
|
|||||||
"author": "作者:",
|
"author": "作者:",
|
||||||
"backToList": "返回插件列表",
|
"backToList": "返回插件列表",
|
||||||
"browseRegistry": "",
|
"browseRegistry": "",
|
||||||
|
"builtinDisabled": "已停用",
|
||||||
"builtinInstalledGlobally": "{{name}} 已全域安裝",
|
"builtinInstalledGlobally": "{{name}} 已全域安裝",
|
||||||
"builtinInstallFailed": "安裝 {{name}} 失敗:{{error}}",
|
"builtinInstallFailed": "安裝 {{name}} 失敗:{{error}}",
|
||||||
"builtinMetadataOnly": "僅內建中繼資料",
|
"builtinMetadataOnly": "僅內建中繼資料",
|
||||||
|
|||||||
4
packages/i18n/src/resources.d.ts
vendored
4
packages/i18n/src/resources.d.ts
vendored
@@ -3040,6 +3040,7 @@ export default interface Resources {
|
|||||||
"savedProbeFailed": "Saved, but probe failed.",
|
"savedProbeFailed": "Saved, but probe failed.",
|
||||||
"settingsSaved": "Settings saved.",
|
"settingsSaved": "Settings saved.",
|
||||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||||
"statusNotDetected": "✗ {{reason}}",
|
"statusNotDetected": "✗ {{reason}}",
|
||||||
"subname": "by Nous Research",
|
"subname": "by Nous Research",
|
||||||
"testFailed": "Test failed — see status above.",
|
"testFailed": "Test failed — see status above.",
|
||||||
@@ -4402,6 +4403,7 @@ export default interface Resources {
|
|||||||
"savedNotFound": "Saved · ✗ openclaw not found",
|
"savedNotFound": "Saved · ✗ openclaw not found",
|
||||||
"savedProbeFailed": "Saved, but probe failed.",
|
"savedProbeFailed": "Saved, but probe failed.",
|
||||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||||
|
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||||
"statusNotFound": "✗ not detected on PATH",
|
"statusNotFound": "✗ not detected on PATH",
|
||||||
"subprocess": "CLI subprocess timeout (ms)",
|
"subprocess": "CLI subprocess timeout (ms)",
|
||||||
"subprocessHint": "Fusion-side hard cap before the CLI subprocess is killed (default: {{default}}s).",
|
"subprocessHint": "Fusion-side hard cap before the CLI subprocess is killed (default: {{default}}s).",
|
||||||
@@ -4465,6 +4467,7 @@ export default interface Resources {
|
|||||||
"statusCliDiscoveryFailed": "CLI discovery failed: {{reason}}",
|
"statusCliDiscoveryFailed": "CLI discovery failed: {{reason}}",
|
||||||
"statusConnected": "Connected",
|
"statusConnected": "Connected",
|
||||||
"statusConnectedAs": "Connected as {{agentName}}{{roleInfo}}{{companyInfo}}",
|
"statusConnectedAs": "Connected as {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||||
|
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||||
"statusProbing": "Probing Paperclip server…",
|
"statusProbing": "Probing Paperclip server…",
|
||||||
"statusUnreachable": "{{reason}}",
|
"statusUnreachable": "{{reason}}",
|
||||||
"tabApi": "API (URL + token)",
|
"tabApi": "API (URL + token)",
|
||||||
@@ -4662,6 +4665,7 @@ export default interface Resources {
|
|||||||
"backToList": "Back to plugin list",
|
"backToList": "Back to plugin list",
|
||||||
"browseRegistry": "Browse Registry",
|
"browseRegistry": "Browse Registry",
|
||||||
"builtinInstallFailed": "Failed to install {{name}}: {{error}}",
|
"builtinInstallFailed": "Failed to install {{name}}: {{error}}",
|
||||||
|
"builtinDisabled": "Disabled",
|
||||||
"builtinInstalledGlobally": "{{name}} installed globally",
|
"builtinInstalledGlobally": "{{name}} installed globally",
|
||||||
"builtinMetadataOnly": "Built-in metadata only",
|
"builtinMetadataOnly": "Built-in metadata only",
|
||||||
"builtinNoPackage": "{{name}} is built in and does not have an installable package yet",
|
"builtinNoPackage": "{{name}} is built in and does not have an installable package yet",
|
||||||
|
|||||||
Reference in New Issue
Block a user