diff --git a/docs/configuration.md b/docs/configuration.md index d290f61ff..b9d0aae69 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2309,18 +2309,8 @@ Disabled skills are excluded from the main agent's skill summary, from always-on ### Agent Plugins v1 nanobot discovers [Agent Plugins](https://agent-plugins.org/) in -`/plugins//`. A v1 package has `plugin.json` and may provide skills, MCP -servers, or both: - -```text -plugins/ -└── release-tools/ - ├── plugin.json - ├── mcp.json - └── skills/ - └── release-notes/ - └── SKILL.md -``` +`/plugins//`. A v1 package has `plugin.json` and may add `mcp.json`, +`skills//SKILL.md`, or both. Directory presence means installed; activation is an explicit trust decision in **Apps**. Enabled skills use normal progressive loading and `$skill-name` invocation. Workspace skills diff --git a/nanobot/agent/plugins.py b/nanobot/agent/plugins.py index 56d574f90..02ce83e92 100644 --- a/nanobot/agent/plugins.py +++ b/nanobot/agent/plugins.py @@ -29,15 +29,6 @@ _SETUP_TIMEOUT_SECONDS = 600 _MAX_LOGO_BYTES = 256 * 1024 -@dataclass(frozen=True) -class AgentPluginSkill: - """One skill supplied by a valid Agent Plugins v1 package.""" - - name: str - path: Path - plugin: str - - @dataclass(frozen=True) class AgentPlugin: """A validated, locally installed Agent Plugins v1 package.""" @@ -71,14 +62,8 @@ def _discover_agent_plugins(workspace: Path) -> list[AgentPlugin]: root = _contained_directory(workspace / "plugins", workspace) if root is None: return [] - try: - candidates = sorted(root.iterdir(), key=lambda path: path.name) - except OSError as exc: - logger.warning("Could not inspect Agent Plugins directory: {}", exc) - return [] - plugins: list[AgentPlugin] = [] - for candidate in candidates: + for candidate in _children(root, "Agent Plugins directory"): plugin_root = _contained_directory(candidate, root) if plugin_root is None: continue @@ -88,13 +73,14 @@ def _discover_agent_plugins(workspace: Path) -> list[AgentPlugin]: return plugins -def enabled_agent_plugin_skills(workspace: Path) -> list[AgentPluginSkill]: +def enabled_agent_plugin_skills(workspace: Path) -> list[tuple[str, Path]]: """Return skills from plugins the user has explicitly enabled.""" - skills: list[AgentPluginSkill] = [] - for plugin in _discover_agent_plugins(workspace): - if _enabled(workspace, plugin.name): - skills.extend(_discover_plugin_skills(plugin.name, plugin.root)) - return skills + return [ + skill + for plugin in _discover_agent_plugins(workspace) + if _enabled(workspace, plugin.name) + for skill in _discover_plugin_skills(plugin.name, plugin.root) + ] def _load_manifest(plugin_root: Path) -> AgentPlugin | None: @@ -172,13 +158,12 @@ def set_agent_plugin_enabled(workspace: Path, name: str, enabled: bool) -> Agent if plugin is None: raise ValueError(f"unknown Agent Plugin '{name}'") data = _plugin_data_dir(workspace, plugin.name, create=True) + version = plugin.version or "unknown" with FileLock(str(data / ".state.lock"), timeout=_SETUP_TIMEOUT_SECONDS + 10): if enabled: - if plugin.install_command and _setup_version(workspace, plugin.name) != ( - plugin.version or "unknown" - ): + if plugin.install_command and _setup_version(workspace, plugin.name) != version: _run_install(plugin, data) - _write_state(data / "setup-version", plugin.version or "unknown") + _write_state(data / "setup-version", version) _write_state(data / "enabled", "1") else: (data / "enabled").unlink(missing_ok=True) @@ -209,12 +194,11 @@ def _plugin_logo(value: object, plugin_root: Path) -> Path | None: try: data = logo.read_bytes() if logo is not None else b"" suffix = logo.suffix.lower() if logo is not None else "" - valid = ( + if len(data) <= _MAX_LOGO_BYTES and ( suffix == ".png" and data.startswith(b"\x89PNG\r\n\x1a\n") or suffix in {".jpg", ".jpeg"} and data.startswith(b"\xff\xd8\xff") or suffix == ".webp" and data.startswith(b"RIFF") and data[8:12] == b"WEBP" - ) - if valid and len(data) <= _MAX_LOGO_BYTES: + ): return logo except OSError: pass @@ -334,7 +318,7 @@ def _plugin_data_dir(workspace: Path, name: str, *, create: bool) -> Path: workspace_id = sha256(str(workspace.expanduser().resolve()).encode()).hexdigest()[:12] config_root = get_config_path().expanduser().resolve().parent plugin_root = _private_directory(config_root / "plugin-data", config_root, create=create) - state_root = _private_directory(plugin_root / workspace_id, config_root, create=create) + state_root = _private_directory(plugin_root / workspace_id, plugin_root, create=create) data = state_root / name return _private_directory(data, state_root, create=True) if create else data @@ -394,44 +378,36 @@ def _run_install(plugin: AgentPlugin, data: Path) -> None: raise RuntimeError(output or f"{plugin.display_name} setup failed") -def _discover_plugin_skills(plugin_name: str, plugin_root: Path) -> list[AgentPluginSkill]: +def _discover_plugin_skills(plugin_name: str, plugin_root: Path) -> list[tuple[str, Path]]: skills_root = _contained_directory(plugin_root / "skills", plugin_root) if skills_root is None: return [] - try: - candidates = sorted(skills_root.iterdir(), key=lambda path: path.name) - except OSError as exc: - logger.warning("Could not inspect Agent Plugin '{}' skills: {}", plugin_name, exc) - return [] - - skills: list[AgentPluginSkill] = [] - for candidate in candidates: + skills: list[tuple[str, Path]] = [] + for candidate in _children(skills_root, f"Agent Plugin '{plugin_name}' skills"): skill_root = _contained_directory(candidate, skills_root) if skill_root is None: continue skill_file = _contained_file(skill_root / "SKILL.md", plugin_root) - if skill_file is None or not _valid_skill(skill_file, candidate.name, plugin_name): + if skill_file is None: continue - skills.append( - AgentPluginSkill(name=candidate.name, path=skill_file, plugin=plugin_name) - ) + try: + metadata = parse_skill_metadata(skill_file.read_text(encoding="utf-8")) + except (OSError, UnicodeError): + metadata = None + if metadata is None or not valid_skill_metadata(metadata, candidate.name): + logger.warning("Ignoring Agent Plugin '{}' skill '{}': invalid metadata", plugin_name, candidate.name) + continue + skills.append((candidate.name, skill_file)) return skills -def _valid_skill(path: Path, directory_name: str, plugin_name: str) -> bool: +def _children(root: Path, label: str) -> list[Path]: try: - content = path.read_text(encoding="utf-8") - except (OSError, UnicodeError): - return False - metadata = parse_skill_metadata(content) - if metadata is None: - logger.warning("Ignoring Agent Plugin '{}' skill '{}': invalid frontmatter", plugin_name, directory_name) - return False - if not valid_skill_metadata(metadata, directory_name): - logger.warning("Ignoring Agent Plugin '{}' skill '{}': invalid metadata", plugin_name, directory_name) - return False - return True + return sorted(root.iterdir(), key=lambda path: path.name) + except OSError as exc: + logger.warning("Could not inspect {}: {}", label, exc) + return [] def _contained_directory(path: Path, root: Path) -> Path | None: diff --git a/nanobot/agent/skills.py b/nanobot/agent/skills.py index bf38e884e..5088f50de 100644 --- a/nanobot/agent/skills.py +++ b/nanobot/agent/skills.py @@ -104,18 +104,17 @@ class SkillsLoader: plugin_skills = enabled_agent_plugin_skills(self.workspace) skills = self._skill_entries_from_dir(self.workspace_skills, "workspace") seen_names = {entry["name"] for entry in skills} - for plugin_skill in plugin_skills: - if plugin_skill.name in seen_names: + for name, path in plugin_skills: + if name in seen_names: continue skills.append( { - "name": plugin_skill.name, - "path": str(plugin_skill.path), + "name": name, + "path": str(path), "source": "plugin", - "plugin": plugin_skill.plugin, } ) - seen_names.add(plugin_skill.name) + seen_names.add(name) if self.builtin_skills and self.builtin_skills.exists(): skills.extend( self._skill_entries_from_dir(self.builtin_skills, "builtin", skip_names=seen_names) diff --git a/nanobot/apps/cli/service.py b/nanobot/apps/cli/service.py index 6a594bcc8..fcb208522 100644 --- a/nanobot/apps/cli/service.py +++ b/nanobot/apps/cli/service.py @@ -212,26 +212,22 @@ def _as_object_dict(value: object) -> dict[str, Any] | None: return cast(dict[str, Any], value) if isinstance(value, dict) else None -def _safe_skill_name(name: str) -> str: - clean = _SAFE_NAME_RE.sub("-", name.lower()).replace("_", "-").strip("-") - return f"cli-app-{clean or 'app'}" - - -def _legacy_skill_name(name: str) -> str: - """Return the workspace skill name emitted before Agent Plugins support.""" +def _skill_name(name: str, *, legacy: bool = False) -> str: clean = _SAFE_NAME_RE.sub("-", name.lower()).strip("-") + if not legacy: + clean = clean.replace("_", "-") return f"cli-app-{clean or 'app'}" def _plugin_skill_relative_path(name: str) -> str: - skill_name = _safe_skill_name(name) + skill_name = _skill_name(name) return f"plugins/{skill_name}/skills/{skill_name}/SKILL.md" def cli_app_skill_relative_path(workspace: Path, name: str) -> str: """Return a CLI App's skill path, including the legacy location.""" canonical = _plugin_skill_relative_path(name) - legacy = f"skills/{_legacy_skill_name(name)}/SKILL.md" + legacy = f"skills/{_skill_name(name, legacy=True)}/SKILL.md" if not (workspace / canonical).is_file() and (workspace / legacy).is_file(): return legacy return canonical @@ -733,7 +729,7 @@ class CliAppManager: entry_point = str(app.get("entry_point") or "") strategy = self._strategy(app) skill_path = _plugin_skill_relative_path(name) - plugin_path = f"plugins/{_safe_skill_name(name)}" + plugin_path = f"plugins/{_skill_name(name)}" capabilities = [ compact_dict({ "type": "cli", @@ -1054,7 +1050,7 @@ class CliAppManager: entry = str(app.get("entry_point") or f"cli-anything-{name}") description = (_catalog_description(app) or f"Use {display} from nanobot.")[:1024] return f"""--- -name: {_safe_skill_name(name)} +name: {_skill_name(name)} description: {json.dumps(description, ensure_ascii=False)} --- @@ -1096,18 +1092,18 @@ Use the `run_cli_app` tool with `name="{name}"` for command execution. Do not in path = self.workspace / _plugin_skill_relative_path(name) path.parent.mkdir(parents=True, exist_ok=True) content = self._fetch_skill_content(app) or self._fallback_skill(app) - content = normalize_skill_document(content, _safe_skill_name(name)) or self._fallback_skill(app) + content = normalize_skill_document(content, _skill_name(name)) or self._fallback_skill(app) content = self._with_nanobot_skill_note(content, app) path.write_text(content, encoding="utf-8") plugin_root = path.parents[2] manifest = compact_dict({ "$schema": AGENT_PLUGIN_SCHEMA, - "name": _safe_skill_name(str(app["name"])), + "name": _skill_name(str(app["name"])), "version": str(app.get("version") or ""), "description": _catalog_description(app), }) _write_json(plugin_root / "plugin.json", manifest) - legacy_dir = self.workspace / "skills" / _legacy_skill_name(str(app["name"])) + legacy_dir = self.workspace / "skills" / _skill_name(str(app["name"]), legacy=True) if legacy_dir.is_dir(): shutil.rmtree(legacy_dir) return path @@ -1116,7 +1112,7 @@ Use the `run_cli_app` tool with `name="{name}"` for command execution. Do not in plugin_root = (self.workspace / _plugin_skill_relative_path(name)).parents[2] if plugin_root.is_dir(): shutil.rmtree(plugin_root) - legacy_dir = self.workspace / "skills" / _legacy_skill_name(name) + legacy_dir = self.workspace / "skills" / _skill_name(name, legacy=True) if legacy_dir.is_dir(): shutil.rmtree(legacy_dir) @@ -1128,7 +1124,7 @@ Use the `run_cli_app` tool with `name="{name}"` for command execution. Do not in installed[str(app["name"])] = entry self._save_installed(installed) self.install_skill(app) - set_agent_plugin_enabled(self.workspace, _safe_skill_name(str(app["name"])), True) + set_agent_plugin_enabled(self.workspace, _skill_name(str(app["name"])), True) return entry def install(self, name: str) -> dict[str, Any]: diff --git a/nanobot/webui/mcp_presets_api.py b/nanobot/webui/mcp_presets_api.py index 702de62ac..32b2b303f 100644 --- a/nanobot/webui/mcp_presets_api.py +++ b/nanobot/webui/mcp_presets_api.py @@ -878,8 +878,6 @@ def _agent_plugin_payload(state: AgentPluginState) -> dict[str, Any]: "brand_color": plugin.accent_color, "required_fields": [], "connection_summary": ", ".join(state.mcp_servers), - "enabled_tools": ["*"], - "tool_names": [], "source": "agent-plugin", } @@ -902,16 +900,12 @@ def mcp_presets_payload( for name, cfg in sorted(config.tools.mcp_servers.items()) if name not in known ] - plugin_states = [ - state - for state in discover_agent_plugin_states(config.workspace_path) - if state.mcp_servers or state.plugin.install_command - ] existing_names = {str(row["name"]) for row in (*preset_rows, *custom_rows)} plugin_rows = [ - row - for state in plugin_states - if (row := _agent_plugin_payload(state))["name"] not in existing_names + _agent_plugin_payload(state) + for state in discover_agent_plugin_states(config.workspace_path) + if (state.mcp_servers or state.plugin.install_command) + and f"plugin-{state.plugin.name}" not in existing_names ] payload: dict[str, Any] = { "presets": [*preset_rows, *custom_rows, *plugin_rows], @@ -1449,16 +1443,13 @@ async def mcp_presets_settings_action( if name.startswith("plugin-"): plugin_config = load_config(config_path) if config_path is not None else load_config() plugin_name = name.removeprefix("plugin-") - plugin_state = next( - ( - state - for state in discover_agent_plugin_states(plugin_config.workspace_path) - if state.plugin.name == plugin_name - and (state.mcp_servers or state.plugin.install_command) - ), - None, - ) - if name not in plugin_config.tools.mcp_servers and plugin_state is not None: + plugin_states = discover_agent_plugin_states(plugin_config.workspace_path) + plugin_state = next((state for state in plugin_states if state.plugin.name == plugin_name), None) + if ( + name not in plugin_config.tools.mcp_servers + and plugin_state is not None + and (plugin_state.mcp_servers or plugin_state.plugin.install_command) + ): if action not in {"enable", "disable"}: raise McpPresetError("Agent Plugins support enable and disable actions only") if ( diff --git a/tests/agent/test_agent_plugins.py b/tests/agent/test_agent_plugins.py index 816b62f08..0cb639d27 100644 --- a/tests/agent/test_agent_plugins.py +++ b/tests/agent/test_agent_plugins.py @@ -80,7 +80,7 @@ def _write_setup_plugin(workspace: Path) -> tuple[Path, Path]: def _loaded_plugin_skills(workspace: Path) -> list[str]: - return [skill.name for skill in enabled_agent_plugin_skills(workspace)] + return [name for name, _ in enabled_agent_plugin_skills(workspace)] def test_skills_loader_discovers_agent_plugin_skill(tmp_path: Path) -> None: @@ -95,7 +95,6 @@ def test_skills_loader_discovers_agent_plugin_skill(tmp_path: Path) -> None: "name": "release-notes", "path": str(plugin / "skills" / "release-notes" / "SKILL.md"), "source": "plugin", - "plugin": "acme-tools", } ] assert loader.get_explicitly_invoked_skills("Use $release-notes") == ["release-notes"] diff --git a/tests/webui/test_mcp_presets_api.py b/tests/webui/test_mcp_presets_api.py index 5a817082f..c377bffa2 100644 --- a/tests/webui/test_mcp_presets_api.py +++ b/tests/webui/test_mcp_presets_api.py @@ -158,24 +158,15 @@ def test_agent_plugin_reuses_mcp_catalog_and_runtime_action( with pytest.raises(McpPresetError, match="enable and disable"): asyncio.run(plugin_action("remove")) - -def test_explicit_mcp_config_wins_over_plugin_catalog_name( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - _use_config(tmp_path, monkeypatch) config_path = tmp_path / "config.json" config = json.loads(config_path.read_text(encoding="utf-8")) config["tools"] = { "mcpServers": {"plugin-desktop": {"type": "stdio", "command": "echo"}} } config_path.write_text(json.dumps(config), encoding="utf-8") - _write_agent_plugin(load_config().workspace_path) - rows = [ item for item in mcp_presets_payload()["presets"] if item["name"] == "plugin-desktop" ] - assert len(rows) == 1 assert rows[0]["source"] == "custom" diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index 11184c56e..534be0a38 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -7701,11 +7701,11 @@ function McpAppsCatalogRow({ const toolsBusy = actionKey === `tools:${preset.name}`; const busy = enableBusy || disableBusy || removeBusy || testBusy || toolsBusy; const agentPlugin = preset.source === "agent-plugin"; + const toggleable = preset.enabled !== undefined; const missingFields = preset.required_fields.filter((field) => field.required && !field.configured); const hasFields = preset.required_fields.length > 0; const needsSetupInput = missingFields.length > 0; - const pluginEnabled = agentPlugin && preset.enabled === true; - const readyInstalled = agentPlugin ? pluginEnabled : preset.installed && preset.configured; + const readyInstalled = preset.enabled ?? (preset.installed && preset.configured); const canEnable = preset.install_supported && (missingFields.length === 0 || missingFields.every((field) => Boolean(values[field.name]?.trim()))); @@ -7717,8 +7717,8 @@ function McpAppsCatalogRow({ const detail = agentPlugin && preset.requires ? `${description} · ${preset.requires}` : description || preset.requires; - const statusLabel = agentPlugin - ? tx("settings.apps.pluginEnabled", "Plugin enabled") + const statusLabel = toggleable + ? tx("settings.nanobotFeatures.enabled", "Enabled") : mcpPresetStatusLabel(preset.status, tx); useEffect(() => { @@ -7754,7 +7754,7 @@ function McpAppsCatalogRow({

{preset.display_name}

{agentPlugin - ? tx("settings.apps.pluginLabel", "Plugin") + ? tx("settings.apps.filterPlugins", "Plugins") : tx("settings.apps.mcpLabel", "Integration")} @@ -7775,31 +7775,31 @@ function McpAppsCatalogRow({ - {!agentPlugin ? ( + {!toggleable ? ( onAction("test", preset.name)}> {tx("settings.mcp.test", "Test")} ) : null} - {!agentPlugin && toolNames.length ? ( + {!toggleable && toolNames.length ? ( setToolsOpen((open) => !open)}> {tx("settings.mcp.toolScope", "Tools")} ) : null} onAction(agentPlugin ? "disable" : "remove", preset.name)} + onClick={() => onAction(toggleable ? "disable" : "remove", preset.name)} > - {agentPlugin ? : } - {agentPlugin - ? tx("settings.apps.pluginDisable", "Disable") + {toggleable ? : } + {toggleable + ? tx("settings.nanobotFeatures.disable", "Disable") : tx("settings.mcp.remove", "Remove")} - {!agentPlugin ? ( + {!toggleable ? ( ) : null} - ) : agentPlugin && preset.installed ? ( + ) : preset.enabled === false ? ( onAction("enable", preset.name, values)} > @@ -8023,8 +8023,7 @@ function appsTitle(item: AppsCatalogItem): string { function appsReady(item: AppsCatalogItem): boolean { if (item.kind === "cli") return item.app.installed; - if (item.preset.source === "agent-plugin") return item.preset.enabled === true; - return item.preset.installed && item.preset.configured; + return item.preset.enabled ?? (item.preset.installed && item.preset.configured); } function appsSearchText(item: AppsCatalogItem): string { diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index afcd0f81f..b0b5637a0 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -585,10 +585,6 @@ "description": "Add tools to nanobot, then @ them in chat.", "cliLabel": "App", "mcpLabel": "Integration", - "pluginLabel": "Plugin", - "pluginEnabled": "Plugin enabled", - "pluginEnable": "Enable plugin", - "pluginDisable": "Disable", "channelLabel": "Channel", "featureLabel": "Feature", "filterAll": "Ready", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index acf018c9a..ae2e9fb3e 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -572,10 +572,6 @@ "description": "Añade herramientas a nanobot y luego úsalas con @ en el chat.", "cliLabel": "Aplicación", "mcpLabel": "Integración", - "pluginLabel": "Plugin", - "pluginEnabled": "Plugin activado", - "pluginEnable": "Activar plugin", - "pluginDisable": "Desactivar", "channelLabel": "Canal", "featureLabel": "Función", "filterAll": "Listo", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 57b2742a3..aa32089c6 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -571,10 +571,6 @@ "description": "Ajoutez des outils à nanobot, puis utilisez-les avec @ dans le chat.", "cliLabel": "Application", "mcpLabel": "Intégration", - "pluginLabel": "Plugin", - "pluginEnabled": "Plugin activé", - "pluginEnable": "Activer le plugin", - "pluginDisable": "Désactiver", "channelLabel": "Canal", "featureLabel": "Fonction", "filterAll": "Prêts", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index b4b7943bf..81326edf9 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -571,10 +571,6 @@ "description": "Tambahkan alat ke nanobot, lalu gunakan dengan @ di chat.", "cliLabel": "Aplikasi", "mcpLabel": "Integrasi", - "pluginLabel": "Plugin", - "pluginEnabled": "Plugin aktif", - "pluginEnable": "Aktifkan plugin", - "pluginDisable": "Nonaktifkan", "channelLabel": "Kanal", "featureLabel": "Fitur", "filterAll": "Siap", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index 97df103c2..08fe99bd7 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -571,10 +571,6 @@ "description": "nanobot にツールを追加し、チャットで @ を付けて使用できます。", "cliLabel": "アプリ", "mcpLabel": "連携", - "pluginLabel": "プラグイン", - "pluginEnabled": "プラグインは有効です", - "pluginEnable": "プラグインを有効にする", - "pluginDisable": "無効にする", "channelLabel": "チャンネル", "featureLabel": "機能", "filterAll": "使用可能", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index d15fbe532..dd3bfdf60 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -571,10 +571,6 @@ "description": "nanobot에 도구를 추가한 뒤 채팅에서 @로 사용하세요.", "cliLabel": "앱", "mcpLabel": "연동", - "pluginLabel": "플러그인", - "pluginEnabled": "플러그인 활성화됨", - "pluginEnable": "플러그인 활성화", - "pluginDisable": "비활성화", "channelLabel": "채널", "featureLabel": "기능", "filterAll": "사용 가능", diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json index eb6c093ad..eef0bea72 100644 --- a/webui/src/i18n/locales/pt-BR/common.json +++ b/webui/src/i18n/locales/pt-BR/common.json @@ -585,10 +585,6 @@ "description": "Adicione ferramentas ao nanobot e mencione-as com @ na conversa.", "cliLabel": "Aplicativo", "mcpLabel": "Integração", - "pluginLabel": "Plugin", - "pluginEnabled": "Plugin ativado", - "pluginEnable": "Ativar plugin", - "pluginDisable": "Desativar", "channelLabel": "Canal", "featureLabel": "Recurso", "filterAll": "Prontos", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index 53cc2204b..98ca6c1b3 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -571,10 +571,6 @@ "description": "Thêm công cụ vào nanobot, sau đó dùng @ trong cuộc trò chuyện.", "cliLabel": "Ứng dụng", "mcpLabel": "Tích hợp", - "pluginLabel": "Plugin", - "pluginEnabled": "Plugin đã bật", - "pluginEnable": "Bật plugin", - "pluginDisable": "Tắt", "channelLabel": "Kênh", "featureLabel": "Tính năng", "filterAll": "Sẵn sàng", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 7e12035a6..265f81797 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -585,10 +585,6 @@ "description": "将工具接入 nanobot,然后在对话中通过 @ 调用。", "cliLabel": "应用", "mcpLabel": "集成", - "pluginLabel": "插件", - "pluginEnabled": "插件已启用", - "pluginEnable": "启用插件", - "pluginDisable": "停用", "channelLabel": "渠道", "featureLabel": "能力", "filterAll": "可用", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index c4355516d..a2eef59f5 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -571,10 +571,6 @@ "description": "將工具新增至 nanobot,再於聊天中使用 @ 指定工具。", "cliLabel": "應用程式", "mcpLabel": "整合", - "pluginLabel": "外掛", - "pluginEnabled": "外掛已啟用", - "pluginEnable": "啟用外掛", - "pluginDisable": "停用", "channelLabel": "通訊管道", "featureLabel": "功能", "filterAll": "就緒", diff --git a/webui/src/lib/mcp-preset-events.ts b/webui/src/lib/mcp-preset-events.ts index aa597d37d..a1b9da6cc 100644 --- a/webui/src/lib/mcp-preset-events.ts +++ b/webui/src/lib/mcp-preset-events.ts @@ -9,7 +9,9 @@ export function isMcpPresetsPayload(value: unknown): value is McpPresetsPayload } export function installedMcpPresetsFromPayload(payload: McpPresetsPayload): McpPresetInfo[] { - return payload.presets.filter((preset) => preset.installed && preset.configured); + return payload.presets.filter( + (preset) => preset.enabled ?? (preset.installed && preset.configured), + ); } export function notifyMcpPresetsChanged(payload: McpPresetsPayload): void { diff --git a/webui/src/tests/settings-view.test.tsx b/webui/src/tests/settings-view.test.tsx index 318058d12..00e441779 100644 --- a/webui/src/tests/settings-view.test.tsx +++ b/webui/src/tests/settings-view.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { SettingsView } from "@/components/settings/SettingsView"; +import { installedMcpPresetsFromPayload } from "@/lib/mcp-preset-events"; import { ClientProvider } from "@/providers/ClientProvider"; import type { ChannelSetupContract, @@ -674,6 +675,7 @@ describe("SettingsView Apps catalog", () => { connection_summary: "computer-use", source: "agent-plugin", }; + expect(installedMcpPresetsFromPayload({ presets: [plugin], installed_count: 0 })).toEqual([]); const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url === "/api/settings") return jsonResponse(settingsPayload()); @@ -701,11 +703,11 @@ describe("SettingsView Apps catalog", () => { renderSettingsView(); expect(await screen.findByText("Computer Use")).toBeInTheDocument(); - expect(screen.getByText("Plugin")).toBeInTheDocument(); + expect(screen.getByText("Plugins")).toBeInTheDocument(); expect( screen.getByText("Control the desktop with a live preview. · screen-recording, accessibility"), ).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Enable plugin" })); + fireEvent.click(screen.getByRole("button", { name: "Enable" })); await waitFor(() => { expect(requestMutationMock).toHaveBeenCalledWith( @@ -715,7 +717,7 @@ describe("SettingsView Apps catalog", () => { ); }); expect(await screen.findByText("Computer Use enabled.")).toBeInTheDocument(); - const enabledButton = screen.getByRole("button", { name: "Plugin enabled" }); + const enabledButton = screen.getByRole("button", { name: "Enabled" }); await waitFor(() => expect(enabledButton).toBeEnabled()); fireEvent.pointerDown(enabledButton, { button: 0, ctrlKey: false }); fireEvent.click(await screen.findByRole("menuitem", { name: "Disable" })); @@ -728,7 +730,7 @@ describe("SettingsView Apps catalog", () => { ); }); expect(await screen.findByText("Computer Use disabled.")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Enable plugin" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Enable" })).toBeInTheDocument(); }); it("keeps runtime dependencies out of Apps and explains chat mentions", async () => {