diff --git a/.changeset/fn-8521-separate-plugin-install-toggle.md b/.changeset/fn-8521-separate-plugin-install-toggle.md new file mode 100644 index 0000000000..e519fd71ef --- /dev/null +++ b/.changeset/fn-8521-separate-plugin-install-toggle.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent plugin toggles from reinstalling uninstalled runtimes. +category: fix +dev: FN-8521 / Runfusion/Fusion#2409 separates install from project-scoped enablement. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 0880c5c9ee..feac50cb25 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1385,7 +1385,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. +- A built-in runtime without an installed plugin record offers **Install** only. Once installed, it exposes project-scoped Enable/Disable, management, and uninstall controls; toggling never installs or reinstalls a runtime. The Runtimes settings cards mirror the installed runtime 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). diff --git a/docs/plugin-management.md b/docs/plugin-management.md index bae2da08e7..2b813ce5fc 100644 --- a/docs/plugin-management.md +++ b/docs/plugin-management.md @@ -135,7 +135,7 @@ 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. +Built-in runtime rows without a `plugin_installs` record expose **Install** only. After installation, their project-scoped **Enable/Disable** toggle and management/uninstall controls become available. Installing and enabling are separate actions: changing enabled state never registers or reinstalls a runtime. The **Runtimes** settings cards (Hermes/OpenClaw/Paperclip) reflect the installed plugin state and show "Disabled in Plugin Manager" when an installed runtime has been turned off. ## 5) Configure plugin settings diff --git a/packages/dashboard/app/components/PluginManager.tsx b/packages/dashboard/app/components/PluginManager.tsx index 39d5c2d000..b5a142dcbe 100644 --- a/packages/dashboard/app/components/PluginManager.tsx +++ b/packages/dashboard/app/components/PluginManager.tsx @@ -289,14 +289,6 @@ export function PluginManager({ addToast, projectId, onPluginsChanged }: PluginM const [builtinSetupStatusById, setBuiltinSetupStatusById] = useState>({}); const [loadingBuiltinSetupId, setLoadingBuiltinSetupId] = useState(null); const [installingBuiltinSetupId, setInstallingBuiltinSetupId] = useState(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(null); const { confirm } = useConfirm(); const loadPlugins = useCallback(async (background = false, mutationResponse?: PluginInstallation) => { @@ -614,44 +606,15 @@ export function PluginManager({ addToast, projectId, onPluginsChanged }: PluginM }; /* - * 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. + * FNXC:PluginManager 2026-07-22-20:41: + * FN-8521 keeps installation and project-scoped enablement as separate actions: only a + * PluginInstallation can reach this toggle, so toggling can never register a missing package. */ - 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 handleToggleBuiltinRuntime = async (installedPlugin: PluginInstallation) => { + if (installedPlugin.enabled) { + await handleDisable(installedPlugin); + } else { + await handleEnable(installedPlugin); } }; @@ -1167,16 +1130,8 @@ export function PluginManager({ addToast, projectId, onPluginsChanged }: PluginM 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; + const runtimeEnabled = installedPlugin?.enabled; return (
@@ -1187,7 +1142,7 @@ export function PluginManager({ addToast, projectId, onPluginsChanged }: PluginM {isInstalled ? t("plugins.statusInstalled", "Installed") : metadataOnly ? t("plugins.statusBuiltIn", "Built in") : t("plugins.statusNotInstalled", "Not installed")} - {isRuntimeBuiltin && !runtimeEnabled && ( + {isRuntimeBuiltin && runtimeEnabled === false && ( {t("plugins.builtinDisabled", "Disabled")} )} {requiresSetupAction && ( @@ -1205,14 +1160,13 @@ export function PluginManager({ addToast, projectId, onPluginsChanged }: PluginM {builtinPlugin.description}
- {isRuntimeBuiltin && ( + {isRuntimeBuiltin && installedPlugin && (