Release WV (v0.51.641): dark-mode disabled-plugin badge contrast + active-first sort (#4848, closes #4713)
Release WV (v0.51.641): dark-mode disabled-plugin badge contrast + active-first sort (#4848, closes #4713)
This commit is contained in:
4 files changed
+111
-1
No files matched your search
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.641] — 2026-06-25 — Release WV (disabled plugins read as disabled in dark mode, active ones sort first)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **In dark mode, disabled plugins no longer show a green "enabled-looking" badge, and active plugins sort to the top of the Plugins list.** The disabled-badge style lost a CSS specificity contest to the dark provider-badge rule, so a disabled plugin's badge rendered the same green as an enabled one — indistinguishable at a glance. A dark-mode-specific rule now renders the disabled badge in the muted surface color. The Plugins list also sorts active/enabled plugins above inactive ones (a stable partition that never reorders within a bucket), with the sort bucket always matching the badge actually shown. Thanks @rodboev. (#4848, closes #4713)
|
||||
|
||||
## [v0.51.640] — 2026-06-25 — Release WU (faster profile switching — Claude Code transcript parses are cached)
|
||||
|
||||
### Fixed
|
||||
|
||||
+27
-1
@@ -8253,6 +8253,32 @@ async function handlePluginEnableToggle(pluginKey, checked){
|
||||
}
|
||||
}
|
||||
|
||||
function _pluginActivationState(plugin){
|
||||
const activation=(plugin&&typeof plugin.activation==='string')
|
||||
? plugin.activation
|
||||
: (plugin&&plugin.enabled===false ? 'disabled' : 'enabled');
|
||||
// Mirror _buildPluginCard's isProviderActive precedence: an explicit
|
||||
// is_active_provider===true overrides the activation string so the sort
|
||||
// bucket always matches the badge.
|
||||
if(plugin&&plugin.is_active_provider===true) return 'provider';
|
||||
if(activation==='exclusive'||activation==='provider'){
|
||||
if(plugin&&plugin.is_active_provider===false) return 'disabled';
|
||||
return 'provider';
|
||||
}
|
||||
if(activation==='enabled') return 'enabled';
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
function _partitionPluginsActiveFirst(plugins){
|
||||
const active=[];
|
||||
const inactive=[];
|
||||
for(const p of plugins){
|
||||
if(_pluginActivationState(p)==='disabled') inactive.push(p);
|
||||
else active.push(p);
|
||||
}
|
||||
return active.concat(inactive);
|
||||
}
|
||||
|
||||
async function loadPluginsPanel(){
|
||||
const list=$('pluginsList');
|
||||
const empty=$('pluginsEmpty');
|
||||
@@ -8271,7 +8297,7 @@ async function loadPluginsPanel(){
|
||||
}
|
||||
if(empty) empty.style.display='none';
|
||||
list.style.display='';
|
||||
for(const plugin of plugins){
|
||||
for(const plugin of _partitionPluginsActiveFirst(plugins)){
|
||||
list.appendChild(_buildPluginCard(plugin));
|
||||
}
|
||||
}catch(e){
|
||||
|
||||
@@ -4654,6 +4654,7 @@ main.main > #mainPlugin{display:none;}
|
||||
.plugin-card-header{cursor:default;}
|
||||
.plugin-card-header:hover{background:transparent;}
|
||||
.plugin-card-badge-disabled{background:var(--surface);color:var(--muted);}
|
||||
:root.dark .provider-card-badge.plugin-card-badge-disabled{background:var(--surface);color:var(--muted);}
|
||||
.plugin-hook-list{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;}
|
||||
.plugin-hook-badge{display:inline-flex;align-items:center;border:1px solid var(--border2);background:var(--code-bg);color:var(--text);border-radius:999px;padding:3px 8px;font-size:11px;font-family:var(--font-mono);}
|
||||
.plugin-hook-empty{font-size:12px;color:var(--muted);font-style:italic;}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Regression checks for Issue #4713: plugin badge and sort-order alignment.
|
||||
|
||||
Focus:
|
||||
- _pluginActivationState must check is_active_provider===true before the
|
||||
activation string branch so the sort bucket always matches the badge shown
|
||||
by _buildPluginCard (Greptile P2 finding on PR #4848).
|
||||
- _buildPluginCard must show the active-provider badge when is_active_provider
|
||||
is true, regardless of the activation string.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PANELS_JS = (Path(__file__).parent.parent / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_block(src: str, name: str) -> str:
|
||||
marker = re.search(rf"(^|\n)(?:async\s+)?function\s+{re.escape(name)}\(", src)
|
||||
assert marker is not None, f"{name}() not found"
|
||||
start = marker.start()
|
||||
next_marker = re.search(r"\n(?:function\s+\w+\(|async\s+function\s+\w+\()", src[start + 1:])
|
||||
end = start + 1 + next_marker.start() if next_marker else len(src)
|
||||
return src[start:end]
|
||||
|
||||
|
||||
def test_activation_state_checks_is_active_provider_before_activation_string():
|
||||
"""is_active_provider===true must be checked before the activation branch."""
|
||||
block = _function_block(PANELS_JS, "_pluginActivationState")
|
||||
pos_early = block.find("is_active_provider===true")
|
||||
pos_branch = block.find("activation==='exclusive'")
|
||||
assert pos_early != -1, "_pluginActivationState must check is_active_provider===true"
|
||||
assert pos_branch != -1, "_pluginActivationState must have the exclusive|provider branch"
|
||||
assert pos_early < pos_branch, (
|
||||
"is_active_provider===true check must precede the activation==='exclusive' branch"
|
||||
)
|
||||
|
||||
|
||||
def test_activation_state_early_guard_returns_provider():
|
||||
"""The early is_active_provider===true guard must return 'provider'."""
|
||||
block = _function_block(PANELS_JS, "_pluginActivationState")
|
||||
match = re.search(r"is_active_provider===true\)?\s*return\s*'provider'", block)
|
||||
assert match is not None, (
|
||||
"_pluginActivationState must return 'provider' when is_active_provider===true"
|
||||
)
|
||||
|
||||
|
||||
def test_build_plugin_card_uses_is_active_provider_boolean():
|
||||
"""_buildPluginCard must read plugin.is_active_provider when it's a boolean."""
|
||||
block = _function_block(PANELS_JS, "_buildPluginCard")
|
||||
assert "is_active_provider" in block, (
|
||||
"_buildPluginCard must reference plugin.is_active_provider"
|
||||
)
|
||||
|
||||
|
||||
def test_activation_state_disabled_plugin_with_active_provider_sorts_active():
|
||||
"""Ensure badge and sort agree: is_active_provider===true wins over activation='disabled'.
|
||||
|
||||
Static code check verifying the fix structure rather than a runtime
|
||||
execution, since panels.js runs in a browser context.
|
||||
"""
|
||||
block = _function_block(PANELS_JS, "_pluginActivationState")
|
||||
lines = block.splitlines()
|
||||
early_return_line = None
|
||||
branch_line = None
|
||||
for i, line in enumerate(lines):
|
||||
if "is_active_provider===true" in line and "return 'provider'" in line:
|
||||
early_return_line = i
|
||||
if early_return_line is None and "is_active_provider===true" in line:
|
||||
if i + 1 < len(lines) and "return 'provider'" in lines[i + 1]:
|
||||
early_return_line = i
|
||||
if "activation==='exclusive'" in line:
|
||||
branch_line = i
|
||||
assert early_return_line is not None, "early is_active_provider===true return not found"
|
||||
assert branch_line is not None, "exclusive|provider branch not found"
|
||||
assert early_return_line < branch_line, (
|
||||
"is_active_provider===true return must precede the exclusive|provider branch"
|
||||
)
|
||||
Reference in new issue
Block a user