stage #4517: plugins panel active-provider badge via is_active_provider (#4496) + v0.51.531 CHANGELOG

This commit is contained in:
nesquena-hermes committed 2026-06-20 03:35:44 +00:00
1 parent 163a805813
commit a60ba78298
5 files changed
+216 -7

No files matched your search

+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.531] — 2026-06-20 — Release SP (plugins panel: correct active-provider badge)
### Fixed
- **The Settings → Plugins panel now shows the green "Active provider" badge only on the provider that's actually selected (#4496).** Exclusive provider plugins (e.g. memory backends) all report `enabled: false` by design, so the panel couldn't tell the *selected* provider from its inactive siblings — it badged any exclusive plugin as the active provider. The payload now carries an `is_active_provider` signal (computed from the category's `<category>.provider` config) and the panel badges only the selected one; unselected exclusive providers render as "Disabled". Flat-key exclusive plugins (where the category can't be inferred) keep the older activation-based badge, so nothing regresses. Thanks @franksong2702.
## [v0.51.530] — 2026-06-20 — Release SO (fix /api/profiles 500)
### Fixed
+48 -4
View File
@@ -7071,6 +7071,31 @@ def _clean_plugin_visibility_text(value, *, limit=240) -> str:
return text
def _plugin_visibility_category_from_key(key: str) -> str:
"""Return the config category prefix for nested plugin keys."""
raw = str(key or "").strip().replace("\\", "/")
if "/" not in raw:
return ""
category = raw.split("/", 1)[0].strip()
return category if category and category not in {".", ".."} else ""
def _plugin_visibility_selected_provider(category: str) -> str:
"""Read ``<category>.provider`` without surfacing config errors."""
if not category:
return ""
try:
from api.config import get_config as _get_cfg
cfg = _get_cfg() or {}
except Exception:
return ""
category_cfg = cfg.get(category, {}) if isinstance(cfg, dict) else {}
if not isinstance(category_cfg, dict):
return ""
return str(category_cfg.get("provider") or "").strip().lower()
def _plugin_visibility_payload(manager=None) -> dict:
"""Build a sanitized plugin/hook visibility payload for Settings.
@@ -7084,8 +7109,12 @@ def _plugin_visibility_payload(manager=None) -> dict:
category's ``<category>.provider`` config, not through ``plugins.enabled``.
Their ``loaded.enabled`` stays False by design and they register hooks
outside the four visibility hooks below. The payload surfaces ``kind``
and ``activation`` so the panel can render them distinctly instead of
mislabeling them as "Disabled" with no hooks (issue #2659).
and ``activation`` plus ``is_active_provider`` when the plugin key carries a
category prefix so the panel can render the selected provider distinctly
instead of mislabeling it as "Disabled" with no hooks (issue #2659), while
still showing unselected exclusive providers as disabled. Flat-key exclusive
plugins omit the new field so older activation-based badge semantics remain
intact when the category cannot be inferred.
"""
manager = manager or _get_plugin_manager_for_visibility()
manager.discover_and_load(force=False)
@@ -7107,19 +7136,31 @@ def _plugin_visibility_payload(manager=None) -> dict:
description = _clean_plugin_visibility_text(getattr(manifest, "description", ""), limit=280)
kind = _clean_plugin_visibility_text(getattr(manifest, "kind", "") or "standalone", limit=40)
enabled_flag = bool(getattr(loaded, "enabled", False))
category = _plugin_visibility_category_from_key(plugin_key)
selected_provider = _plugin_visibility_selected_provider(category)
plugin_slug = plugin_key.rsplit("/", 1)[-1].strip().lower()
if kind == "exclusive":
activation = "exclusive"
elif kind == "model-provider" and enabled_flag:
activation = "provider"
else:
activation = "enabled" if enabled_flag else "disabled"
include_active_provider = True
if kind == "exclusive":
if category:
is_active_provider = bool(selected_provider) and plugin_slug == selected_provider
else:
include_active_provider = False
is_active_provider = False
else:
is_active_provider = kind == "model-provider" and enabled_flag
registered = []
for hook in list(getattr(manifest, "provides_hooks", []) or []) + list(getattr(loaded, "hooks_registered", []) or []):
hook_name = str(hook or "").strip()
if hook_name in _PLUGIN_VISIBILITY_HOOK_SET and hook_name not in registered:
registered.append(hook_name)
registered.sort(key=_PLUGIN_VISIBILITY_HOOKS.index)
plugins.append({
plugin_payload = {
"name": name,
"key": plugin_key or name,
"version": version,
@@ -7130,7 +7171,10 @@ def _plugin_visibility_payload(manager=None) -> dict:
"kind": kind,
"activation": activation,
"hooks": registered,
})
}
if include_active_provider:
plugin_payload["is_active_provider"] = bool(is_active_provider)
plugins.append(plugin_payload)
return {
"plugins": plugins,
+4 -1
View File
@@ -7565,9 +7565,12 @@ const enabled=plugin&&plugin.enabled!==false;
</label>
</div>`
: '');
const isProviderActive = plugin&&typeof plugin.is_active_provider==='boolean'
? plugin.is_active_provider
: isProvider;
let badgeText;
let badgeClass;
if(isProvider){
if(isProviderActive){
badgeText=t('plugins_active_provider');
badgeClass='plugin-card-badge-provider';
}else if(activation==='enabled'){
+104
View File
@@ -0,0 +1,104 @@
"""Regression coverage for #4496 plugin provider badge state."""
import json
import subprocess
import textwrap
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def test_provider_badge_uses_active_provider_payload_state(tmp_path):
script = tmp_path / "check_plugin_badge.js"
script.write_text(
textwrap.dedent(
f"""
const fs = require('fs');
const assert = require('assert');
const src = fs.readFileSync({json.dumps(str(REPO_ROOT / "static" / "panels.js"))}, 'utf8');
function extractFunction(name) {{
const marker = 'function ' + name;
const start = src.indexOf(marker);
if (start < 0) throw new Error('missing function ' + name);
const brace = src.indexOf('{{', start);
let depth = 1;
let i = brace + 1;
while (depth && i < src.length) {{
if (src[i] === '{{') depth += 1;
else if (src[i] === '}}') depth -= 1;
i += 1;
}}
return src.slice(start, i);
}}
global.t = (key) => key;
global.esc = (value) => String(value ?? '');
global.document = {{
createElement() {{
return {{
className: '',
dataset: {{}},
_html: '',
set innerHTML(value) {{ this._html = String(value); }},
get innerHTML() {{ return this._html; }},
querySelector() {{ return null; }},
}};
}},
}};
eval(extractFunction('_buildPluginCard'));
const activeExclusive = _buildPluginCard({{
key: 'memory',
name: 'Memory',
activation: 'exclusive',
is_active_provider: true,
enabled: false,
hooks: [],
}}).innerHTML;
assert(activeExclusive.includes('plugin-card-badge-provider'), activeExclusive);
assert(!activeExclusive.includes('plugin-card-badge-disabled'), activeExclusive);
assert(activeExclusive.includes('plugins_active_provider'), activeExclusive);
const legacyFlatKeyExclusive = _buildPluginCard({{
key: 'noema',
name: 'Noema',
activation: 'exclusive',
enabled: false,
hooks: [],
}}).innerHTML;
assert(legacyFlatKeyExclusive.includes('plugin-card-badge-provider'), legacyFlatKeyExclusive);
assert(!legacyFlatKeyExclusive.includes('plugin-card-badge-disabled'), legacyFlatKeyExclusive);
assert(legacyFlatKeyExclusive.includes('plugins_active_provider'), legacyFlatKeyExclusive);
const inactiveExclusive = _buildPluginCard({{
key: 'memory',
name: 'Memory',
activation: 'exclusive',
is_active_provider: false,
enabled: false,
hooks: [],
}}).innerHTML;
assert(inactiveExclusive.includes('plugin-card-badge-disabled'), inactiveExclusive);
assert(!inactiveExclusive.includes('plugin-card-badge-provider'), inactiveExclusive);
assert(inactiveExclusive.includes('plugins_disabled'), inactiveExclusive);
const activeModelProvider = _buildPluginCard({{
key: 'openrouter',
name: 'OpenRouter',
activation: 'provider',
is_active_provider: true,
enabled: true,
hooks: [],
}}).innerHTML;
assert(activeModelProvider.includes('plugin-card-badge-provider'), activeModelProvider);
assert(!activeModelProvider.includes('plugin-card-badge-disabled'), activeModelProvider);
assert(activeModelProvider.includes('plugins_active_provider'), activeModelProvider);
"""
),
encoding="utf-8",
)
subprocess.run(["node", str(script)], check=True, cwd=REPO_ROOT)
+54 -2
View File
@@ -90,6 +90,7 @@ class TestPluginsApi:
"enabled": True,
"kind": "standalone",
"activation": "enabled",
"is_active_provider": False,
"hooks": ["pre_tool_call", "post_tool_call"],
}]
serialized = repr(payload)
@@ -147,7 +148,7 @@ class TestPluginsApi:
"noema": _FakeLoadedPlugin(
_FakeManifest(
name="noema",
key="noema",
key="memory/noema",
version="0.1.0",
description="Structured memory backed by a Noema Cortex",
kind="exclusive",
@@ -158,11 +159,13 @@ class TestPluginsApi:
)
})
payload = self._capture_plugins_response(manager)
with patch("api.config.get_config", return_value={"memory": {"provider": "noema"}}):
payload = self._capture_plugins_response(manager)
plugin = payload["plugins"][0]
assert plugin["kind"] == "exclusive"
assert plugin["activation"] == "exclusive"
assert plugin["is_active_provider"] is True
# `enabled` stays False for back-compat with older WebUI clients that
# key off it directly; new clients must read `activation`.
assert plugin["enabled"] is False
@@ -171,6 +174,54 @@ class TestPluginsApi:
# filesystem paths or other internals on other plugin kinds.
assert "exclusive plugin" not in repr(payload)
def test_api_plugins_marks_unselected_exclusive_plugins_inactive(self):
manager = _FakePluginManager({
"honcho": _FakeLoadedPlugin(
_FakeManifest(
name="honcho",
key="memory/honcho",
version="1.0.0",
description="Honcho memory provider",
kind="exclusive",
),
enabled=False,
hooks_registered=[],
)
})
with patch("api.config.get_config", return_value={"memory": {"provider": "noema"}}):
payload = self._capture_plugins_response(manager)
plugin = payload["plugins"][0]
assert plugin["kind"] == "exclusive"
assert plugin["activation"] == "exclusive"
assert plugin["enabled"] is False
assert plugin["is_active_provider"] is False
def test_api_plugins_omits_active_provider_for_flat_key_exclusive_plugins(self):
manager = _FakePluginManager({
"noema": _FakeLoadedPlugin(
_FakeManifest(
name="noema",
key="noema",
version="0.1.0",
description="Flat-key memory provider",
kind="exclusive",
),
enabled=False,
hooks_registered=[],
)
})
with patch("api.config.get_config", return_value={"memory": {"provider": "noema"}}):
payload = self._capture_plugins_response(manager)
plugin = payload["plugins"][0]
assert plugin["kind"] == "exclusive"
assert plugin["activation"] == "exclusive"
assert plugin["enabled"] is False
assert "is_active_provider" not in plugin
def test_api_plugins_marks_active_model_provider(self):
# model-provider plugins that loaded successfully (loaded.enabled True)
# get the "provider" activation badge so the panel can distinguish
@@ -194,6 +245,7 @@ class TestPluginsApi:
plugin = payload["plugins"][0]
assert plugin["kind"] == "model-provider"
assert plugin["activation"] == "provider"
assert plugin["is_active_provider"] is True
assert plugin["enabled"] is True