diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 353fc8e608..0bb200430b 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -3617,12 +3617,16 @@ def _get_dashboard_plugins(force_rescan: bool = False) -> list: @app.get("/api/dashboard/plugins") async def get_dashboard_plugins(): - """Return discovered dashboard plugins.""" + """Return discovered dashboard plugins (excludes user-hidden ones).""" plugins = _get_dashboard_plugins() - # Strip internal fields before sending to frontend. + # Read user's hidden plugins list from config. + config = load_config() + hidden: list = cfg_get(config, "dashboard", "hidden_plugins", default=[]) or [] + # Strip internal fields before sending to frontend and filter out hidden. return [ {k: v for k, v in p.items() if not k.startswith("_")} for p in plugins + if p["name"] not in hidden ] @@ -3662,6 +3666,10 @@ def _merged_plugins_hub() -> Dict[str, Any]: disabled_set = _get_disabled_set() enabled_set = _get_enabled_set() + # Read user-hidden plugins from config for the user_hidden field. + config = load_config() + hidden_plugins: list = cfg_get(config, "dashboard", "hidden_plugins", default=[]) or [] + plugins_root_resolved = (get_hermes_home() / "plugins").resolve() rows: List[Dict[str, Any]] = [] @@ -3718,6 +3726,7 @@ def _merged_plugins_hub() -> Dict[str, Any]: "can_update_git": can_remove_update and (Path(dir_str) / ".git").exists(), "auth_required": auth_required, "auth_command": auth_command, + "user_hidden": name in hidden_plugins, }) agent_names = {r["name"] for r in rows} @@ -3863,6 +3872,33 @@ async def put_plugin_providers(request: Request, body: _PluginProvidersPutBody): return {"ok": True} +class _PluginVisibilityBody(BaseModel): + hidden: bool + + +@app.post("/api/dashboard/plugins/{name}/visibility") +async def post_plugin_visibility(request: Request, name: str, body: _PluginVisibilityBody): + """Toggle a plugin's sidebar visibility (persists to config.yaml dashboard.hidden_plugins).""" + _require_token(request) + name = _validate_plugin_name(name) + + config = load_config() + if "dashboard" not in config or not isinstance(config.get("dashboard"), dict): + config["dashboard"] = {} + hidden_list: list = config["dashboard"].get("hidden_plugins") or [] + if not isinstance(hidden_list, list): + hidden_list = [] + + if body.hidden and name not in hidden_list: + hidden_list.append(name) + elif not body.hidden and name in hidden_list: + hidden_list.remove(name) + + config["dashboard"]["hidden_plugins"] = hidden_list + save_config(config) + return {"ok": True, "name": name, "hidden": body.hidden} + + @app.get("/dashboard-plugins/{plugin_name}/{file_path:path}") async def serve_plugin_asset(plugin_name: str, file_path: str): """Serve static assets from a dashboard plugin directory. diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 9c0b92ca6d..55e3267b1b 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -295,6 +295,8 @@ export const en: Translations = { authRequiredHint: "Run this command to authenticate:", updateGit: "Git pull", versionBadge: "Version", + showInSidebar: "Show in sidebar", + hideFromSidebar: "Hide from sidebar", }, skills: { diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index 4e67d7e9a4..d93260d26d 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -266,6 +266,8 @@ export interface Translations { authRequiredHint: string; updateGit: string; versionBadge: string; + showInSidebar: string; + hideFromSidebar: string; }; // ── Profiles page ── diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 6eb726d483..b64de0661f 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -291,6 +291,8 @@ export const zh: Translations = { authRequiredHint: "运行此命令以完成认证:", updateGit: "git pull", versionBadge: "版本", + showInSidebar: "在侧边栏显示", + hideFromSidebar: "从侧边栏隐藏", }, skills: { diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 89cffea197..8fed709765 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -299,6 +299,16 @@ export const api = { body: JSON.stringify(body), }), + setPluginVisibility: (name: string, hidden: boolean) => + fetchJSON<{ ok: boolean; name: string; hidden: boolean }>( + `/api/dashboard/plugins/${encodeURIComponent(name)}/visibility`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ hidden }), + }, + ), + // Dashboard themes getThemes: () => fetchJSON("/api/dashboard/themes"), @@ -728,6 +738,7 @@ export interface HubAgentPluginRow { can_update_git: boolean; auth_required: boolean; auth_command: string; + user_hidden: boolean; } export interface PluginsHubProviders { diff --git a/web/src/pages/PluginsPage.tsx b/web/src/pages/PluginsPage.tsx index c11fce2e51..17123cd9e3 100644 --- a/web/src/pages/PluginsPage.tsx +++ b/web/src/pages/PluginsPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { ExternalLink, RefreshCw, Puzzle, Trash2 } from "lucide-react"; +import { ExternalLink, RefreshCw, Puzzle, Trash2, Eye, EyeOff } from "lucide-react"; import type { Translations } from "@/i18n/types"; import { Link } from "react-router-dom"; import { api } from "@/lib/api"; @@ -504,6 +504,27 @@ function PluginRowCard(props: PluginRowCardProps) { ) : null} + {row.has_dashboard_manifest ? ( + + ) : null} + {row.can_remove ? (