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
|
||||
- Inspect plugin runtime state and transition feedback
|
||||
- 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).
|
||||
|
||||
|
||||
@@ -133,6 +133,10 @@ fn plugin disable <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
|
||||
|
||||
1. Go to **Settings → Plugins → Fusion Plugins**.
|
||||
|
||||
@@ -923,6 +923,52 @@ export default plugin;
|
||||
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 () => {
|
||||
await pluginStore.init();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
fetchHermesProfiles,
|
||||
fetchHermesStatus,
|
||||
fetchPluginSettings,
|
||||
fetchPlugins,
|
||||
updatePluginSettings,
|
||||
type HermesProfileSummary,
|
||||
type HermesProviderStatus,
|
||||
@@ -72,6 +73,14 @@ export function HermesRuntimeCard() {
|
||||
const [profiles, setProfiles] = useState<HermesProfileSummary[]>([]);
|
||||
const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | 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);
|
||||
|
||||
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(() => {
|
||||
setBusy("loading");
|
||||
fetchPluginSettings(PLUGIN_ID)
|
||||
@@ -198,13 +221,16 @@ export function HermesRuntimeCard() {
|
||||
}, [buildPayload, probe, t]);
|
||||
|
||||
const binary = status?.binary;
|
||||
const statusKind = status === null
|
||||
? "loading"
|
||||
: binary?.available
|
||||
? "ok"
|
||||
: "err";
|
||||
const statusText =
|
||||
status === null
|
||||
const statusKind = runtimeDisabled
|
||||
? "neutral"
|
||||
: status === null
|
||||
? "loading"
|
||||
: binary?.available
|
||||
? "ok"
|
||||
: "err";
|
||||
const statusText = runtimeDisabled
|
||||
? t("hermes.statusDisabledInPluginManager", "Disabled in Plugin Manager")
|
||||
: status === null
|
||||
? t("hermes.probing", "Probing local hermes binary…")
|
||||
: binary?.available
|
||||
? 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 {
|
||||
fetchOpenClawStatus,
|
||||
fetchPluginSettings,
|
||||
fetchPlugins,
|
||||
updatePluginSettings,
|
||||
type OpenClawProviderStatus,
|
||||
} from "../api";
|
||||
@@ -83,6 +84,13 @@ export function OpenClawRuntimeCard() {
|
||||
const [status, setStatus] = useState<OpenClawProviderStatus | null>(null);
|
||||
const [busy, setBusy] = useState<"loading" | "saving" | "testing" | "save-test" | 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);
|
||||
|
||||
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.
|
||||
useEffect(() => {
|
||||
setBusy("loading");
|
||||
@@ -201,10 +223,12 @@ export function OpenClawRuntimeCard() {
|
||||
}, [buildPayload, probe, t]);
|
||||
|
||||
const binary = status?.binary;
|
||||
const statusKind =
|
||||
status === null ? "loading" : binary?.available ? "ok" : "err";
|
||||
const statusText =
|
||||
status === null
|
||||
const statusKind = runtimeDisabled
|
||||
? "neutral"
|
||||
: status === null ? "loading" : binary?.available ? "ok" : "err";
|
||||
const statusText = runtimeDisabled
|
||||
? t("openclaw.statusDisabledInPluginManager", "Disabled in Plugin Manager")
|
||||
: status === null
|
||||
? t("openclaw.probing", "Probing local openclaw binary…")
|
||||
: binary?.available
|
||||
? t("openclaw.statusDetected", `✓ Detected{{version}}{{path}}`, { version: binary.version ? ` ${binary.version}` : "", path: binary.binaryPath ? ` · ${binary.binaryPath}` : "" })
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
fetchPaperclipCompanies,
|
||||
fetchPaperclipStatus,
|
||||
fetchPluginSettings,
|
||||
fetchPlugins,
|
||||
mintPaperclipApiKey,
|
||||
updatePluginSettings,
|
||||
type PaperclipAgentSummary,
|
||||
@@ -132,6 +133,13 @@ export function PaperclipRuntimeCard() {
|
||||
>(null);
|
||||
const [toast, setToast] = useState<{ kind: "ok" | "err"; message: string } | null>(null);
|
||||
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);
|
||||
|
||||
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.
|
||||
// In CLI mode we prefer whatever cliDiscovery returned, falling back to the
|
||||
// typed apiUrl if discovery hasn't completed yet.
|
||||
@@ -411,15 +433,17 @@ export function PaperclipRuntimeCard() {
|
||||
const identity = status?.connection.identity;
|
||||
const cliOk = cliDiscovery?.ok === true;
|
||||
|
||||
const statusKind =
|
||||
status === null
|
||||
const statusKind = runtimeDisabled
|
||||
? "neutral"
|
||||
: status === null
|
||||
? "loading"
|
||||
: connected
|
||||
? "ok"
|
||||
: "err";
|
||||
|
||||
const statusText =
|
||||
status === null
|
||||
const statusText = runtimeDisabled
|
||||
? t("paperclip.statusDisabledInPluginManager", "Disabled in Plugin Manager")
|
||||
: status === null
|
||||
? settings.transport === "cli" && cliDiscovery && !cliOk
|
||||
? t("paperclip.statusCliDiscoveryFailed", "✗ CLI discovery failed: {{reason}}", { reason: cliDiscovery.reason })
|
||||
: t("paperclip.statusProbing", "Probing Paperclip server…")
|
||||
|
||||
@@ -480,6 +480,20 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -831,6 +845,26 @@
|
||||
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 {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -274,6 +274,14 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
||||
const [builtinSetupStatusById, setBuiltinSetupStatusById] = useState<Record<string, PluginSetupStatusResponse>>({});
|
||||
const [loadingBuiltinSetupId, setLoadingBuiltinSetupId] = 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 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) => {
|
||||
try {
|
||||
setReloadingPluginId(plugin.id);
|
||||
@@ -1073,6 +1123,16 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
||||
const setupReady = isInstalled && setupStatus?.hasSetup && pluginSetupState === "installed";
|
||||
const setupCheckInFlight = loadingBuiltinSetupId === builtinPlugin.id;
|
||||
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 (
|
||||
<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"}`}>
|
||||
{isInstalled ? t("plugins.statusInstalled", "Installed") : metadataOnly ? t("plugins.statusBuiltIn", "Built in") : t("plugins.statusNotInstalled", "Not installed")}
|
||||
</span>
|
||||
{isRuntimeBuiltin && !runtimeEnabled && (
|
||||
<span className="plugin-builtins-setup-status plugin-builtins-setup-status--warning">{t("plugins.builtinDisabled", "Disabled")}</span>
|
||||
)}
|
||||
{requiresSetupAction && (
|
||||
<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>
|
||||
</div>
|
||||
{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>
|
||||
<div className="plugin-builtins-actions">
|
||||
{isRuntimeBuiltin && (
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={runtimeEnabled}
|
||||
onChange={() => void handleToggleBuiltinRuntime(builtinPlugin, installedPlugin)}
|
||||
disabled={isTogglingRuntime}
|
||||
aria-label={runtimeEnabled
|
||||
? t("plugins.disablePlugin", "Disable {{name}}", { name: builtinPlugin.name })
|
||||
: t("plugins.enablePlugin", "Enable {{name}}", { name: builtinPlugin.name })}
|
||||
/>
|
||||
<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={() => {
|
||||
if (!isInstalled) {
|
||||
void handleInstallBuiltinPlugin(builtinPlugin);
|
||||
return;
|
||||
}
|
||||
<button
|
||||
className={`btn ${(isInstalled && !requiresSetupAction) ? "btn-secondary" : "btn-primary"} btn-sm`}
|
||||
onClick={() => {
|
||||
if (!isInstalled) {
|
||||
void handleInstallBuiltinPlugin(builtinPlugin);
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresSetupAction) {
|
||||
void handleInstallBuiltinSetup(builtinPlugin);
|
||||
return;
|
||||
}
|
||||
if (requiresSetupAction) {
|
||||
void handleInstallBuiltinSetup(builtinPlugin);
|
||||
return;
|
||||
}
|
||||
|
||||
if (installedPlugin) {
|
||||
void handleSelectPlugin(installedPlugin);
|
||||
if (installedPlugin) {
|
||||
void handleSelectPlugin(installedPlugin);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
installingBuiltinPluginId === builtinPlugin.id
|
||||
|| installingBuiltinSetupId === builtinPlugin.id
|
||||
|| setupCheckInFlight
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
installingBuiltinPluginId === builtinPlugin.id
|
||||
|| installingBuiltinSetupId === builtinPlugin.id
|
||||
|| setupCheckInFlight
|
||||
}
|
||||
>
|
||||
{!isInstalled
|
||||
? (installingBuiltinPluginId === builtinPlugin.id ? t("plugins.installing", "Installing...") : t("plugins.installNamed", "Install {{name}}", { name: builtinPlugin.name }))
|
||||
: requiresSetupAction
|
||||
? (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 }))
|
||||
: 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();
|
||||
});
|
||||
|
||||
const toggle = screen.getByRole("checkbox");
|
||||
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||
expect(toggle).toBeTruthy();
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
@@ -796,7 +796,7 @@ describe("PluginManager", () => {
|
||||
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(() => {
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
@@ -818,7 +818,7 @@ describe("PluginManager", () => {
|
||||
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(() => {
|
||||
expect(addToast).toHaveBeenCalledWith("Failed to enable plugin: network", "error");
|
||||
@@ -835,7 +835,7 @@ describe("PluginManager", () => {
|
||||
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).toBeChecked();
|
||||
|
||||
@@ -1150,7 +1150,7 @@ describe("PluginManager", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const toggle = screen.getByRole("checkbox");
|
||||
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||
expect(toggle).toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -1183,7 +1183,7 @@ describe("PluginManager", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const toggle = screen.getByRole("checkbox");
|
||||
const toggle = screen.getByRole("checkbox", { name: /Test Plugin A/ });
|
||||
expect(toggle).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { PluginManager } from "../PluginManager";
|
||||
import { loadAllAppCss } from "../../test/cssFixture";
|
||||
@@ -21,7 +21,8 @@ vi.mock("../../api", () => ({
|
||||
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();
|
||||
|
||||
@@ -71,6 +72,22 @@ afterEach(() => {
|
||||
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", () => {
|
||||
it("keeps checkbox focusable but visually hidden", async () => {
|
||||
vi.mocked(fetchPlugins).mockResolvedValue([plugin(true)]);
|
||||
@@ -117,3 +134,86 @@ describe("PluginManager toggle switch", () => {
|
||||
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.",
|
||||
"settingsSaved": "Settings saved.",
|
||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "by Nous Research",
|
||||
"testFailed": "Test failed — see status above.",
|
||||
@@ -4401,6 +4402,7 @@
|
||||
"savedNotFound": "Saved · ✗ openclaw not found",
|
||||
"savedProbeFailed": "Saved, but probe failed.",
|
||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||
"statusNotFound": "✗ not detected on PATH",
|
||||
"subprocess": "CLI subprocess timeout (ms)",
|
||||
"subprocessHint": "Fusion-side hard cap before the CLI subprocess is killed (default: {{default}}s).",
|
||||
@@ -4464,6 +4466,7 @@
|
||||
"statusCliDiscoveryFailed": "CLI discovery failed: {{reason}}",
|
||||
"statusConnected": "Connected",
|
||||
"statusConnectedAs": "Connected as {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||
"statusProbing": "Probing Paperclip server…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API (URL + token)",
|
||||
@@ -4660,6 +4663,7 @@
|
||||
"author": "Author:",
|
||||
"backToList": "Back to plugin list",
|
||||
"browseRegistry": "Browse Registry",
|
||||
"builtinDisabled": "Disabled",
|
||||
"builtinInstalledGlobally": "{{name}} installed globally",
|
||||
"builtinInstallFailed": "Failed to install {{name}}: {{error}}",
|
||||
"builtinMetadataOnly": "Built-in metadata only",
|
||||
|
||||
@@ -3029,6 +3029,7 @@
|
||||
"savedProbeFailed": "Guardado, pero la prueba falló.",
|
||||
"settingsSaved": "Configuración guardada.",
|
||||
"statusDetected": "✓ Detectado{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Deshabilitado en el gestor de complementos",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "por Nous Research",
|
||||
"testFailed": "La prueba falló — consulte el estado anterior.",
|
||||
@@ -4391,6 +4392,7 @@
|
||||
"savedNotFound": "Guardado · ✗ openclaw no encontrado",
|
||||
"savedProbeFailed": "Guardado, pero la prueba falló.",
|
||||
"statusDetected": "✓ Detectado{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Deshabilitado en el gestor de complementos",
|
||||
"statusNotFound": "✗ no detectado en PATH",
|
||||
"subprocess": "Tiempo de espera del subproceso CLI (ms)",
|
||||
"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}}",
|
||||
"statusConnected": "Conectado",
|
||||
"statusConnectedAs": "Conectado como {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||
"statusDisabledInPluginManager": "Deshabilitado en el gestor de complementos",
|
||||
"statusProbing": "Sondeando servidor de Paperclip…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API (URL + token)",
|
||||
@@ -4650,6 +4653,7 @@
|
||||
"author": "Autor:",
|
||||
"backToList": "Volver a la lista de plugins",
|
||||
"browseRegistry": "",
|
||||
"builtinDisabled": "Deshabilitado",
|
||||
"builtinInstalledGlobally": "{{name}} instalado globalmente",
|
||||
"builtinInstallFailed": "Error al instalar {{name}}: {{error}}",
|
||||
"builtinMetadataOnly": "Solo metadatos integrados",
|
||||
|
||||
@@ -3029,6 +3029,7 @@
|
||||
"savedProbeFailed": "Enregistré, mais la sonde a échoué.",
|
||||
"settingsSaved": "Paramètres enregistrés.",
|
||||
"statusDetected": "✓ Détecté{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Désactivé dans le gestionnaire de plugins",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "par Nous Research",
|
||||
"testFailed": "Le test a échoué — voir le statut ci-dessus.",
|
||||
@@ -4391,6 +4392,7 @@
|
||||
"savedNotFound": "Enregistré · ✗ openclaw non trouvé",
|
||||
"savedProbeFailed": "Enregistré, mais la sonde a échoué.",
|
||||
"statusDetected": "✓ Détecté{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Désactivé dans le gestionnaire de plugins",
|
||||
"statusNotFound": "✗ non détecté sur PATH",
|
||||
"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).",
|
||||
@@ -4454,6 +4456,7 @@
|
||||
"statusCliDiscoveryFailed": "Échec de la découverte CLI : {{reason}}",
|
||||
"statusConnected": "Connecté",
|
||||
"statusConnectedAs": "Connecté en tant que {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||
"statusDisabledInPluginManager": "Désactivé dans le gestionnaire de plugins",
|
||||
"statusProbing": "Sondage du serveur Paperclip…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API (URL + jeton)",
|
||||
@@ -4650,6 +4653,7 @@
|
||||
"author": "Auteur :",
|
||||
"backToList": "Retour à la liste des plugins",
|
||||
"browseRegistry": "",
|
||||
"builtinDisabled": "Désactivé",
|
||||
"builtinInstalledGlobally": "{{name}} installé globalement",
|
||||
"builtinInstallFailed": "Échec de l'installation de {{name}} : {{error}}",
|
||||
"builtinMetadataOnly": "Métadonnées intégrées uniquement",
|
||||
|
||||
@@ -3029,6 +3029,7 @@
|
||||
"savedProbeFailed": "저장됨, 하지만 탐색에 실패했습니다.",
|
||||
"settingsSaved": "설정이 저장되었습니다.",
|
||||
"statusDetected": "✓ 감지됨{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "플러그인 관리자에서 비활성화됨",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "by Nous Research",
|
||||
"testFailed": "테스트 실패 — 위의 상태를 확인하세요.",
|
||||
@@ -4391,6 +4392,7 @@
|
||||
"savedNotFound": "저장됨 · ✗ openclaw를 찾을 수 없음",
|
||||
"savedProbeFailed": "저장되었지만, 확인에 실패했습니다.",
|
||||
"statusDetected": "✓ 감지됨{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "플러그인 관리자에서 비활성화됨",
|
||||
"statusNotFound": "✗ PATH에서 감지되지 않음",
|
||||
"subprocess": "CLI 서브프로세스 타임아웃 (ms)",
|
||||
"subprocessHint": "CLI 서브프로세스가 종료되기 전 Fusion 측 최대 대기 시간 (기본값: {{default}}s).",
|
||||
@@ -4454,6 +4456,7 @@
|
||||
"statusCliDiscoveryFailed": "CLI 검색에 실패했습니다: {{reason}}",
|
||||
"statusConnected": "연결됨",
|
||||
"statusConnectedAs": "{{agentName}}{{roleInfo}}{{companyInfo}}(으)로 연결됨",
|
||||
"statusDisabledInPluginManager": "플러그인 관리자에서 비활성화됨",
|
||||
"statusProbing": "Paperclip 서버 확인 중…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API (URL + 토큰)",
|
||||
@@ -4650,6 +4653,7 @@
|
||||
"author": "작성자:",
|
||||
"backToList": "플러그인 목록으로 돌아가기",
|
||||
"browseRegistry": "",
|
||||
"builtinDisabled": "비활성화됨",
|
||||
"builtinInstalledGlobally": "{{name}} 전역 설치됨",
|
||||
"builtinInstallFailed": "{{name}} 설치 실패: {{error}}",
|
||||
"builtinMetadataOnly": "기본 제공 메타데이터만",
|
||||
|
||||
@@ -3029,6 +3029,7 @@
|
||||
"savedProbeFailed": "已保存,但探测失败。",
|
||||
"settingsSaved": "设置已保存。",
|
||||
"statusDetected": "✓ 已检测{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "已在插件管理器中禁用",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "由 Nous Research 提供",
|
||||
"testFailed": "测试失败 — 见上方状态。",
|
||||
@@ -4391,6 +4392,7 @@
|
||||
"savedNotFound": "已保存 · ✗ openclaw 未找到",
|
||||
"savedProbeFailed": "已保存,但探测失败。",
|
||||
"statusDetected": "✓ 已检测到{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "已在插件管理器中禁用",
|
||||
"statusNotFound": "✗ 在 PATH 上未检测到",
|
||||
"subprocess": "CLI 子进程超时(毫秒)",
|
||||
"subprocessHint": "Fusion 端限制,超时后 CLI 子进程被终止(默认:{{default}}s)。",
|
||||
@@ -4454,6 +4456,7 @@
|
||||
"statusCliDiscoveryFailed": "CLI 发现失败:{{reason}}",
|
||||
"statusConnected": "已连接",
|
||||
"statusConnectedAs": "已连接为 {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||
"statusDisabledInPluginManager": "已在插件管理器中禁用",
|
||||
"statusProbing": "正在探测 Paperclip 服务器…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API(URL + 令牌)",
|
||||
@@ -4650,6 +4653,7 @@
|
||||
"author": "作者:",
|
||||
"backToList": "返回插件列表",
|
||||
"browseRegistry": "",
|
||||
"builtinDisabled": "已禁用",
|
||||
"builtinInstalledGlobally": "{{name}} 已全局安装",
|
||||
"builtinInstallFailed": "安装 {{name}} 失败:{{error}}",
|
||||
"builtinMetadataOnly": "仅内置元数据",
|
||||
|
||||
@@ -3029,6 +3029,7 @@
|
||||
"savedProbeFailed": "已保存,但探測失敗。",
|
||||
"settingsSaved": "設置已保存。",
|
||||
"statusDetected": "✓ 已偵測{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "已在外掛程式管理器中停用",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "由 Nous Research 提供",
|
||||
"testFailed": "測試失敗 — 見上方狀態。",
|
||||
@@ -4391,6 +4392,7 @@
|
||||
"savedNotFound": "已保存 · ✗ openclaw 未找到",
|
||||
"savedProbeFailed": "已保存,但探測失敗。",
|
||||
"statusDetected": "✓ 已偵測到{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "已在外掛程式管理器中停用",
|
||||
"statusNotFound": "✗ 在 PATH 上未偵測到",
|
||||
"subprocess": "CLI 子進程逾時(毫秒)",
|
||||
"subprocessHint": "Fusion 端限制,逾時後 CLI 子進程被終止(預設:{{default}}s)。",
|
||||
@@ -4454,6 +4456,7 @@
|
||||
"statusCliDiscoveryFailed": "CLI 發現失敗:{{reason}}",
|
||||
"statusConnected": "已連線",
|
||||
"statusConnectedAs": "已連線為 {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||
"statusDisabledInPluginManager": "已在外掛程式管理器中停用",
|
||||
"statusProbing": "正在探測 Paperclip 伺服器…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API(URL + 令牌)",
|
||||
@@ -4650,6 +4653,7 @@
|
||||
"author": "作者:",
|
||||
"backToList": "返回插件列表",
|
||||
"browseRegistry": "",
|
||||
"builtinDisabled": "已停用",
|
||||
"builtinInstalledGlobally": "{{name}} 已全域安裝",
|
||||
"builtinInstallFailed": "安裝 {{name}} 失敗:{{error}}",
|
||||
"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.",
|
||||
"settingsSaved": "Settings saved.",
|
||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||
"statusNotDetected": "✗ {{reason}}",
|
||||
"subname": "by Nous Research",
|
||||
"testFailed": "Test failed — see status above.",
|
||||
@@ -4402,6 +4403,7 @@ export default interface Resources {
|
||||
"savedNotFound": "Saved · ✗ openclaw not found",
|
||||
"savedProbeFailed": "Saved, but probe failed.",
|
||||
"statusDetected": "✓ Detected{{version}}{{path}}",
|
||||
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||
"statusNotFound": "✗ not detected on PATH",
|
||||
"subprocess": "CLI subprocess timeout (ms)",
|
||||
"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}}",
|
||||
"statusConnected": "Connected",
|
||||
"statusConnectedAs": "Connected as {{agentName}}{{roleInfo}}{{companyInfo}}",
|
||||
"statusDisabledInPluginManager": "Disabled in Plugin Manager",
|
||||
"statusProbing": "Probing Paperclip server…",
|
||||
"statusUnreachable": "{{reason}}",
|
||||
"tabApi": "API (URL + token)",
|
||||
@@ -4662,6 +4665,7 @@ export default interface Resources {
|
||||
"backToList": "Back to plugin list",
|
||||
"browseRegistry": "Browse Registry",
|
||||
"builtinInstallFailed": "Failed to install {{name}}: {{error}}",
|
||||
"builtinDisabled": "Disabled",
|
||||
"builtinInstalledGlobally": "{{name}} installed globally",
|
||||
"builtinMetadataOnly": "Built-in metadata only",
|
||||
"builtinNoPackage": "{{name}} is built in and does not have an installable package yet",
|
||||
|
||||
Reference in New Issue
Block a user