Add skill visibility controls

Let users hide skills from the model-facing available catalog through the chat Skills selector while keeping pinned skill injection as a separate mode. Hidden skills are filtered from skill listing, search, loading, relevant recall, and loaded-skill prompt injection, with chat-level show/hide overrides and persistent default hidden-skill config support.

Alessandro committed May 22, 2026 at 17:44 UTC 5e2c2a86efe10e33d883313c5167cfa0a44ab76f
11 files changed +696 -54
extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
+5
@@ -18,9 +18,14 @@ class IncludeLoadedSkills(Extension):
18
19 # load skill text here
20 content = ""
21 + visible_skill_names = []
22 for skill_name in skill_names:
23 + if not skills.find_skill(skill_name, agent=self.agent):
24 + continue
25 + visible_skill_names.append(skill_name)
26 skill_data = skills.load_skill_for_agent(skill_name=skill_name, agent=self.agent)
27 content += "\n\n" + skill_data
28 + self.agent.data[DATA_NAME_LOADED_SKILLS] = visible_skill_names
29 content = content.strip()
30 if not content:
31 return
helpers/skills.py
+271 -18
@@ -23,6 +23,7 @@ ACTIVE_SKILLS_PLUGIN_NAME = "_skills"
23 AGENT_DATA_NAME_LOADED_SKILLS = "loaded_skills"
24 CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS = "skills_chat_active"
25 CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS = "skills_chat_disabled"
26 +CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS = "skills_chat_visible"
27
28
29 class ActiveSkillEntry(TypedDict, total=False):
@@ -35,6 +36,7 @@ class CatalogSkill(TypedDict):
36 description: str
37 path: str
38 origin: str
39 + hidden: bool
40
41
42 @dataclass(slots=True)
@@ -322,6 +324,7 @@ def skill_from_markdown(
324 def list_skills(
325 agent:Agent|None=None,
326 include_content: bool = False,
327 + include_hidden: bool = False,
328 ) -> List[Skill]:
329 """List skills, optionally filtered by agent scope."""
330 skills: List[Skill] = []
@@ -345,7 +348,10 @@ def list_skills(
348 if key and key not in by_name:
349 by_name[key] = s
350
348 - return list(by_name.values())
351 + result = list(by_name.values())
352 + if include_hidden:
353 + return result
354 + return _filter_hidden_skills(agent, result)
355
356
357 def delete_skill(
@@ -380,6 +386,7 @@ def find_skill(
386 skill_name: str,
387 agent:Agent|None=None,
388 include_content: bool = False,
389 + include_hidden: bool = False,
390 ) -> Optional[Skill]:
391 target = _normalize_name(skill_name)
392 if not target:
@@ -393,6 +400,8 @@ def find_skill(
400 if not s:
401 continue
402 if _normalize_name(s.name) == target or _normalize_name(s.path.name) == target:
403 + if not include_hidden and _skill_is_hidden_for_agent(agent, s):
404 + continue
405 return s
406 return None
407
@@ -470,6 +479,7 @@ def search_skills(
479 query: str,
480 limit: int = 25,
481 agent: Agent|None=None,
482 + include_hidden: bool = False,
483 ) -> List[Skill]:
484 q = (query or "").strip().lower()
485 if not q:
@@ -480,7 +490,7 @@ def search_skills(
490 t for t in raw_terms
491 if len(t) >= 3 or any(ch.isdigit() for ch in t)
492 ] or raw_terms
483 - candidates = list_skills(agent)
493 + candidates = list_skills(agent, include_hidden=include_hidden)
494
495 scored: List[Tuple[int, Skill]] = []
496 for s in candidates:
@@ -580,10 +590,25 @@ def normalize_skills_config(config: dict[str, Any] | None) -> dict[str, Any]:
590 normalized["active_skills"] = normalize_active_skills(
591 normalized.get("active_skills")
592 )
593 + normalized["hidden_skills"] = normalize_hidden_skills(
594 + normalized.get("hidden_skills")
595 + )
596 return normalized
597
598
599 def normalize_active_skills(raw: Any) -> list[ActiveSkillEntry]:
600 + return normalize_skill_entries(raw, limit=get_max_active_skills())
601 +
602 +
603 +def normalize_hidden_skills(raw: Any) -> list[ActiveSkillEntry]:
604 + return normalize_skill_entries(raw, limit=None)
605 +
606 +
607 +def normalize_skill_entries(
608 + raw: Any,
609 + *,
610 + limit: int | None = None,
611 +) -> list[ActiveSkillEntry]:
612 if not isinstance(raw, list):
613 return []
614
@@ -601,7 +626,7 @@ def normalize_active_skills(raw: Any) -> list[ActiveSkillEntry]:
626
627 seen.add(key)
628 normalized.append(entry)
604 - if len(normalized) >= get_max_active_skills():
629 + if limit is not None and len(normalized) >= limit:
630 break
631
632 return normalized
@@ -616,6 +641,7 @@ def list_skill_catalog(
641
642 catalog: list[CatalogSkill] = []
643 seen_paths: set[str] = set()
644 + hidden_entries = get_hidden_skills(agent) if agent else []
645
646 for root in _get_catalog_roots(project_name=project_name, agent=agent):
647 root_path = Path(root)
@@ -638,6 +664,7 @@ def list_skill_catalog(
664 runtime_path,
665 project_name=project_name,
666 ),
667 + "hidden": _skill_matches_entries(skill, hidden_entries),
668 }
669 )
670
@@ -662,6 +689,23 @@ def get_scope_active_skills(agent: Agent | None) -> list[ActiveSkillEntry]:
689 return normalize_active_skills(config.get("active_skills"))
690
691
692 +def get_scope_hidden_skills(agent: Agent | None) -> list[ActiveSkillEntry]:
693 + if not agent:
694 + return []
695 +
696 + project_name = _get_agent_project_name(agent)
697 + config = (
698 + plugin_helpers.get_plugin_config(
699 + ACTIVE_SKILLS_PLUGIN_NAME,
700 + agent=agent,
701 + project_name=project_name,
702 + agent_profile="",
703 + )
704 + or {}
705 + )
706 + return normalize_hidden_skills(config.get("hidden_skills"))
707 +
708 +
709 def get_chat_active_skills(context: Any | None) -> list[ActiveSkillEntry]:
710 if not context:
711 return []
@@ -671,16 +715,37 @@ def get_chat_active_skills(context: Any | None) -> list[ActiveSkillEntry]:
715 def get_chat_disabled_skills(context: Any | None) -> list[ActiveSkillEntry]:
716 if not context:
717 return []
674 - return normalize_active_skills(
718 + return normalize_hidden_skills(
719 context.get_data(CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS)
720 )
721
722
723 +def get_chat_visible_skills(context: Any | None) -> list[ActiveSkillEntry]:
724 + if not context:
725 + return []
726 + return normalize_hidden_skills(
727 + context.get_data(CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS)
728 + )
729 +
730 +
731 +def get_hidden_skills(agent: Agent | None) -> list[ActiveSkillEntry]:
732 + if not agent:
733 + return []
734 +
735 + context = getattr(agent, "context", None)
736 + return _merge_hidden_skill_entries(
737 + get_scope_hidden_skills(agent),
738 + get_chat_disabled_skills(context),
739 + get_chat_visible_skills(context),
740 + )
741 +
742 +
743 def _build_active_skills(
744 agent: Agent | None,
745 *,
746 chat_entries: list[ActiveSkillEntry] | None = None,
683 - disabled_entries: list[ActiveSkillEntry] | None = None,
747 + hidden_entries: list[ActiveSkillEntry] | None = None,
748 + visible_entries: list[ActiveSkillEntry] | None = None,
749 limit: int | None = None,
750 ) -> list[ActiveSkillEntry]:
751 if not agent:
@@ -692,15 +757,25 @@ def _build_active_skills(
757 current_chat_entries = list(
758 chat_entries if chat_entries is not None else get_chat_active_skills(context)
759 )
695 - current_disabled_entries = list(
696 - disabled_entries
697 - if disabled_entries is not None
760 + current_hidden_entries = list(
761 + hidden_entries
762 + if hidden_entries is not None
763 else get_chat_disabled_skills(context)
764 )
765 + current_visible_entries = list(
766 + visible_entries
767 + if visible_entries is not None
768 + else get_chat_visible_skills(context)
769 + )
770 + effective_hidden_entries = _merge_hidden_skill_entries(
771 + get_scope_hidden_skills(agent),
772 + current_hidden_entries,
773 + current_visible_entries,
774 + )
775 return _merge_active_skill_entries(
776 scope_entries,
777 current_chat_entries,
703 - current_disabled_entries,
778 + effective_hidden_entries,
779 limit=effective_limit,
780 )
781
@@ -766,19 +841,27 @@ def activate_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
841 for item in get_chat_active_skills(context)
842 if not _entries_match(item, normalized)
843 ]
769 - disabled_entries = [
844 + hidden_entries = [
845 item
846 for item in get_chat_disabled_skills(context)
847 if not _entries_match(item, normalized)
848 ]
849 + visible_entries = [
850 + item
851 + for item in get_chat_visible_skills(context)
852 + if not _entries_match(item, normalized)
853 + ]
854
855 if not any(_entries_match(item, normalized) for item in scope_entries):
856 chat_entries.append(normalized)
857 + if _entry_matches_any(normalized, get_scope_hidden_skills(agent)):
858 + visible_entries.append(normalized)
859
860 merged_entries = _build_active_skills(
861 agent,
862 chat_entries=chat_entries,
781 - disabled_entries=disabled_entries,
863 + hidden_entries=hidden_entries,
864 + visible_entries=visible_entries,
865 limit=-1,
866 )
867 if len(merged_entries) > get_max_active_skills():
@@ -791,10 +874,15 @@ def activate_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
874 CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS,
875 chat_entries,
876 )
794 - _store_context_active_skill_entries(
877 + _store_context_hidden_skill_entries(
878 context,
879 CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS,
797 - disabled_entries,
880 + hidden_entries,
881 + )
882 + _store_context_hidden_skill_entries(
883 + context,
884 + CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS,
885 + visible_entries,
886 )
887 return get_active_skills(agent)
888
@@ -813,38 +901,129 @@ def deactivate_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
901 for item in get_chat_active_skills(context)
902 if not _entries_match(item, normalized)
903 ]
816 - disabled_entries = [
904 + hidden_entries = [
905 item
906 for item in get_chat_disabled_skills(context)
907 if not _entries_match(item, normalized)
908 ]
909 + visible_entries = [
910 + item
911 + for item in get_chat_visible_skills(context)
912 + if not _entries_match(item, normalized)
913 + ]
914
915 is_scope_default = any(
916 _entries_match(item, normalized) for item in get_scope_active_skills(agent)
917 )
918 if is_scope_default:
826 - disabled_entries.append(normalized)
919 + hidden_entries.append(normalized)
920
921 _store_context_active_skill_entries(
922 context,
923 CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS,
924 chat_entries,
925 )
833 - _store_context_active_skill_entries(
926 + _store_context_hidden_skill_entries(
927 context,
928 CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS,
836 - disabled_entries,
929 + hidden_entries,
930 + )
931 + _store_context_hidden_skill_entries(
932 + context,
933 + CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS,
934 + visible_entries,
935 )
936 return get_active_skills(agent)
937
938
939 +def hide_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
940 + normalized = _normalize_active_skill_entry(entry)
941 + if not normalized:
942 + raise ValueError("A skill name or path is required.")
943 +
944 + context = getattr(agent, "context", None)
945 + if not context:
946 + raise ValueError("A chat context is required.")
947 +
948 + chat_entries = [
949 + item
950 + for item in get_chat_active_skills(context)
951 + if not _entries_match(item, normalized)
952 + ]
953 + hidden_entries = [
954 + item
955 + for item in get_chat_disabled_skills(context)
956 + if not _entries_match(item, normalized)
957 + ]
958 + hidden_entries.append(normalized)
959 + visible_entries = [
960 + item
961 + for item in get_chat_visible_skills(context)
962 + if not _entries_match(item, normalized)
963 + ]
964 +
965 + _store_context_active_skill_entries(
966 + context,
967 + CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS,
968 + chat_entries,
969 + )
970 + _store_context_hidden_skill_entries(
971 + context,
972 + CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS,
973 + hidden_entries,
974 + )
975 + _store_context_hidden_skill_entries(
976 + context,
977 + CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS,
978 + visible_entries,
979 + )
980 + unload_agent_skill(agent, normalized)
981 + return get_hidden_skills(agent)
982 +
983 +
984 +def show_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
985 + normalized = _normalize_active_skill_entry(entry)
986 + if not normalized:
987 + raise ValueError("A skill name or path is required.")
988 +
989 + context = getattr(agent, "context", None)
990 + if not context:
991 + raise ValueError("A chat context is required.")
992 +
993 + hidden_entries = [
994 + item
995 + for item in get_chat_disabled_skills(context)
996 + if not _entries_match(item, normalized)
997 + ]
998 + visible_entries = [
999 + item
1000 + for item in get_chat_visible_skills(context)
1001 + if not _entries_match(item, normalized)
1002 + ]
1003 + if _entry_matches_any(normalized, get_scope_hidden_skills(agent)):
1004 + visible_entries.append(normalized)
1005 +
1006 + _store_context_hidden_skill_entries(
1007 + context,
1008 + CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS,
1009 + hidden_entries,
1010 + )
1011 + _store_context_hidden_skill_entries(
1012 + context,
1013 + CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS,
1014 + visible_entries,
1015 + )
1016 + return get_hidden_skills(agent)
1017 +
1018 +
1019 def clear_chat_skill_overrides(agent: Agent) -> list[ActiveSkillEntry]:
1020 context = getattr(agent, "context", None)
1021 if not context:
1022 raise ValueError("A chat context is required.")
1023
1024 _store_context_active_skill_entries(context, CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS, [])
847 - _store_context_active_skill_entries(context, CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS, [])
1025 + _store_context_hidden_skill_entries(context, CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS, [])
1026 + _store_context_hidden_skill_entries(context, CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS, [])
1027 return get_active_skills(agent)
1028
1029
@@ -940,6 +1119,13 @@ def _entries_match(left: ActiveSkillEntry, right: ActiveSkillEntry) -> bool:
1119 return bool(_entry_keys(left) & _entry_keys(right))
1120
1121
1122 +def _entry_matches_any(
1123 + entry: ActiveSkillEntry,
1124 + entries: list[ActiveSkillEntry],
1125 +) -> bool:
1126 + return any(_entries_match(item, entry) for item in entries)
1127 +
1128 +
1129 def _get_agent_project_name(agent: Agent | None) -> str:
1130 context = getattr(agent, "context", None)
1131 if not context:
@@ -1006,6 +1192,28 @@ def _merge_active_skill_entries(
1192 return merged
1193
1194
1195 +def _merge_hidden_skill_entries(
1196 + scope_entries: list[ActiveSkillEntry],
1197 + chat_hidden_entries: list[ActiveSkillEntry],
1198 + chat_visible_entries: list[ActiveSkillEntry],
1199 +) -> list[ActiveSkillEntry]:
1200 + merged: list[ActiveSkillEntry] = []
1201 + seen: set[str] = set()
1202 + visible_keys = {
1203 + key for entry in chat_visible_entries for key in _entry_keys(entry)
1204 + }
1205 +
1206 + for entry in [*scope_entries, *chat_hidden_entries]:
1207 + keys = _entry_keys(entry)
1208 + key = _entry_key(entry)
1209 + if not key or keys & seen or keys & visible_keys:
1210 + continue
1211 + seen.update(keys)
1212 + merged.append(entry)
1213 +
1214 + return merged
1215 +
1216 +
1217 def _store_context_active_skill_entries(
1218 context: Any,
1219 key: str,
@@ -1015,6 +1223,15 @@ def _store_context_active_skill_entries(
1223 context.set_data(key, normalized_entries or None)
1224
1225
1226 +def _store_context_hidden_skill_entries(
1227 + context: Any,
1228 + key: str,
1229 + entries: list[ActiveSkillEntry],
1230 +) -> None:
1231 + normalized_entries = normalize_hidden_skills(entries)
1232 + context.set_data(key, normalized_entries or None)
1233 +
1234 +
1235 def _resolve_active_skill_entries(
1236 agent: Agent | None,
1237 entries: list[ActiveSkillEntry],
@@ -1090,3 +1307,39 @@ def _load_skill_from_runtime_path(
1307 return None
1308
1309 return skill_from_markdown(skill_md, include_content=True)
1310 +
1311 +
1312 +def _skill_entry(skill: Skill) -> ActiveSkillEntry:
1313 + return {
1314 + "name": skill.name or skill.path.name,
1315 + "path": files.normalize_a0_path(str(skill.path)),
1316 + }
1317 +
1318 +
1319 +def _skill_matches_entries(
1320 + skill: Skill,
1321 + entries: list[ActiveSkillEntry],
1322 +) -> bool:
1323 + skill_entry = _skill_entry(skill)
1324 + return any(_entries_match(skill_entry, entry) for entry in entries)
1325 +
1326 +
1327 +def _skill_is_hidden_for_agent(agent: Agent | None, skill: Skill) -> bool:
1328 + if not agent:
1329 + return False
1330 + return _skill_matches_entries(skill, get_hidden_skills(agent))
1331 +
1332 +
1333 +def _filter_hidden_skills(
1334 + agent: Agent | None,
1335 + skills: list[Skill],
1336 +) -> list[Skill]:
1337 + if not agent:
1338 + return skills
1339 +
1340 + hidden_entries = get_hidden_skills(agent)
1341 + if not hidden_entries:
1342 + return skills
1343 + return [
1344 + skill for skill in skills if not _skill_matches_entries(skill, hidden_entries)
1345 + ]
plugins/_skills/README.md
+2
@@ -5,6 +5,7 @@ Skills is a built-in Agent Zero plugin that manages active skills across scope d
5 ## What It Does
6
7 - pins default skills for the current plugin scope
8 +- hides noisy skills from the model-facing available catalog, skill search, and load access
9 - injects the effective active skills into prompt extras on every turn
10 - extends the same config screen with a current-chat mode so users can activate or hide skills live per conversation
11 - supports global and project scoped configurations without agent-profile variants
@@ -22,6 +23,7 @@ The shared active-skill state and prompt-resolution logic live in `helpers/skill
23
24 - keep the active list short because every active skill is injected into prompt extras every turn
25 - the framework-wide cap is 20 active skills
26 +- hidden skills are not capped because they are stored as control data, not injected into the prompt
27 - selected skills are stored in normalized `/a0/...` form so configs stay portable across development and Docker-style layouts
28 - scope defaults can be hidden or supplemented per chat without creating a new conversation
29 - if a configured skill is not visible in the current agent scope, it is skipped quietly instead of breaking the prompt build
plugins/_skills/api/skills_catalog.py
+90 -1
@@ -22,6 +22,10 @@ class SkillsCatalog(ApiHandler):
22 return self._activate(input, context_id=context_id)
23 if action == "deactivate":
24 return self._deactivate(input, context_id=context_id)
25 + if action == "hide":
26 + return self._hide(input, context_id=context_id)
27 + if action == "show":
28 + return self._show(input, context_id=context_id)
29 if action == "clear":
30 return self._clear(context_id=context_id)
31 if action == "get_doc":
@@ -45,6 +49,20 @@ class SkillsCatalog(ApiHandler):
49 save_tmp_chat(context)
50 return self._build_state(context_id=context.id)
51
52 + def _hide(self, input: dict, *, context_id: str) -> dict[str, Any]:
53 + context = self._require_context(context_id)
54 + skill_entry = self._require_skill_entry(input)
55 + skills.hide_chat_skill(context.get_agent(), skill_entry)
56 + save_tmp_chat(context)
57 + return self._build_state(context_id=context.id)
58 +
59 + def _show(self, input: dict, *, context_id: str) -> dict[str, Any]:
60 + context = self._require_context(context_id)
61 + skill_entry = self._require_skill_entry(input)
62 + skills.show_chat_skill(context.get_agent(), skill_entry)
63 + save_tmp_chat(context)
64 + return self._build_state(context_id=context.id)
65 +
66 def _clear(self, *, context_id: str) -> dict[str, Any]:
67 context = self._require_context(context_id)
68 skills.clear_chat_skill_overrides(context.get_agent())
@@ -70,11 +88,17 @@ class SkillsCatalog(ApiHandler):
88 }
89
90 scope_entries = skills.get_scope_active_skills(agent)
91 + scope_hidden_entries = skills.get_scope_hidden_skills(agent)
92 chat_entries = skills.get_chat_active_skills(context)
93 disabled_entries = skills.get_chat_disabled_skills(context)
94 + visible_entries = skills.get_chat_visible_skills(context)
95 + hidden_entries = skills.get_hidden_skills(agent)
96 active_entries = self._merge_entries(
97 skills.get_active_skills(agent),
77 - self._get_loaded_skill_entries(agent),
98 + self._filter_hidden_entries(
99 + self._get_loaded_skill_entries(agent),
100 + hidden_entries,
101 + ),
102 )
103
104 scope_keys = {
@@ -134,6 +158,37 @@ class SkillsCatalog(ApiHandler):
158 )
159 for entry in disabled_entries
160 ],
161 + "hidden_skills": [
162 + self._serialize_entry(
163 + entry,
164 + catalog_by_key,
165 + catalog_by_name,
166 + state_source=(
167 + "Hidden default"
168 + if self._entry_matches_any(entry, scope_hidden_entries)
169 + else "Hidden in chat"
170 + ),
171 + )
172 + for entry in hidden_entries
173 + ],
174 + "scope_hidden_skills": [
175 + self._serialize_entry(
176 + entry,
177 + catalog_by_key,
178 + catalog_by_name,
179 + state_source="Hidden default",
180 + )
181 + for entry in scope_hidden_entries
182 + ],
183 + "visible_skills": [
184 + self._serialize_entry(
185 + entry,
186 + catalog_by_key,
187 + catalog_by_name,
188 + state_source="Visible in chat",
189 + )
190 + for entry in visible_entries
191 + ],
192 }
193
194 def _get_doc(
@@ -218,6 +273,40 @@ class SkillsCatalog(ApiHandler):
273
274 return merged
275
276 + def _filter_hidden_entries(
277 + self,
278 + entries: list[dict[str, Any]],
279 + hidden_entries: list[dict[str, Any]],
280 + ) -> list[dict[str, Any]]:
281 + return [
282 + entry
283 + for entry in entries
284 + if not self._entry_matches_any(entry, hidden_entries)
285 + ]
286 +
287 + def _entry_matches_any(
288 + self,
289 + entry: dict[str, Any],
290 + entries: list[dict[str, Any]],
291 + ) -> bool:
292 + keys = {
293 + str(entry.get("path") or "").strip().lower(),
294 + str(entry.get("name") or "").strip().lower(),
295 + }
296 + keys.discard("")
297 + if not keys:
298 + return False
299 +
300 + for candidate in entries:
301 + candidate_keys = {
302 + str(candidate.get("path") or "").strip().lower(),
303 + str(candidate.get("name") or "").strip().lower(),
304 + }
305 + candidate_keys.discard("")
306 + if keys & candidate_keys:
307 + return True
308 + return False
309 +
310 def _serialize_entry(
311 self,
312 entry: dict[str, Any],
plugins/_skills/default_config.yaml
+1
@@ -1 +1,2 @@
1 active_skills: []
2 +hidden_skills: []
plugins/_skills/extensions/webui/initFw_end/skills-menu-injector.js
+5 -1
@@ -18,7 +18,11 @@ function buildButton() {
18 button.addEventListener("click", async () => {
19 chatInputStore.closeChatMoreMenu();
20 const projectName = chatsStore.selectedContext?.project?.name || "";
21 - await pluginSettingsStore.openConfig("_skills", projectName, "", { focus: "chat" });
21 + await pluginSettingsStore.openConfig("_skills", projectName, "", {
22 + focus: "chat",
23 + hideSettingsActions: true,
24 + title: "Skills",
25 + });
26 });
27
28 return button;
plugins/_skills/webui/config-store.js
+155 -24
@@ -34,31 +34,44 @@ function entryKey(entry) {
34 return String(entry.path || entry.name || "").trim().toLowerCase();
35 }
36
37 +function entryKeys(entry) {
38 + return [entry?.path, entry?.name]
39 + .map((value) => String(value || "").trim().toLowerCase())
40 + .filter(Boolean);
41 +}
42 +
43 +function entriesMatch(left, right) {
44 + const rightKeys = new Set(entryKeys(right));
45 + if (rightKeys.size === 0) return false;
46 + return entryKeys(left).some((key) => rightKeys.has(key));
47 +}
48 +
49 function ensureConfig(config) {
50 if (!config || typeof config !== "object") return;
51 const activeSkills = Array.isArray(config.active_skills) ? config.active_skills : [];
52 + const hiddenSkills = Array.isArray(config.hidden_skills) ? config.hidden_skills : [];
53 +
54 + config.active_skills = compactEntries(activeSkills, MAX_ACTIVE_SKILLS_FALLBACK);
55 + config.hidden_skills = compactEntries(hiddenSkills);
56 +}
57 +
58 +function compactEntries(entries, limit = null) {
59 const normalized = [];
60 const seen = new Set();
61
43 - for (const item of activeSkills) {
62 + for (const item of entries || []) {
63 const entry = normalizeEntry(item);
64 const key = entryKey(entry);
65 if (!entry || !key || seen.has(key)) continue;
66 seen.add(key);
48 - normalized.push(entry);
49 - }
50 -
51 - config.active_skills = normalized;
52 -}
53 -
54 -function compactEntries(entries) {
55 - return entries
56 - .map((entry) => normalizeEntry(entry))
57 - .filter(Boolean)
58 - .map((entry) => ({
67 + normalized.push({
68 ...(entry.name ? { name: entry.name } : {}),
69 ...(entry.path ? { path: entry.path } : {}),
61 - }));
70 + });
71 + if (limit !== null && normalized.length >= limit) break;
72 + }
73 +
74 + return normalized;
75 }
76
77 window.createSkillsConfigModel = (context, config) => ({
@@ -66,13 +79,21 @@ window.createSkillsConfigModel = (context, config) => ({
79 mutatingChat: false,
80 catalog: [],
81 search: "",
82 + mode: "pinned",
83 maxActiveSkills: MAX_ACTIVE_SKILLS_FALLBACK,
84 selectedSkills: [],
85 + hiddenSkills: [],
86 chatContextAvailable: false,
87
88 initDefaults() {
89 ensureConfig(config);
90 + this.mode = this.isChatMode ? "visible" : "pinned";
91 this.selectedSkills = [...this.activeEntries];
92 + this.hiddenSkills = [...this.hiddenEntries];
93 + },
94 +
95 + get isChatMode() {
96 + return context?.openOptions?.focus === "chat";
97 },
98
99 get activeEntries() {
@@ -80,6 +101,11 @@ window.createSkillsConfigModel = (context, config) => ({
101 return config.active_skills;
102 },
103
104 + get hiddenEntries() {
105 + ensureConfig(config);
106 + return config.hidden_skills;
107 + },
108 +
109 get selectedCount() {
110 return this.selectedSkills.length;
111 },
@@ -88,6 +114,22 @@ window.createSkillsConfigModel = (context, config) => ({
114 return `${this.selectedCount} / ${this.maxActiveSkills}`;
115 },
116
117 + get hiddenCount() {
118 + return this.catalog.filter((skill) => this.isHidden(skill)).length;
119 + },
120 +
121 + get visibleCount() {
122 + return Math.max(0, this.catalog.length - this.hiddenCount);
123 + },
124 +
125 + get visibilityCountLabel() {
126 + return `${this.visibleCount} visible / ${this.hiddenCount} hidden`;
127 + },
128 +
129 + get currentCountLabel() {
130 + return this.mode === "visible" ? this.visibilityCountLabel : this.selectedCountLabel;
131 + },
132 +
133 get catalogMap() {
134 const byKey = new Map();
135 for (const skill of this.catalog) {
@@ -121,11 +163,39 @@ window.createSkillsConfigModel = (context, config) => ({
163 return entryKey(entry);
164 },
165
166 + setMode(mode) {
167 + if (!["visible", "pinned"].includes(mode)) return;
168 + this.mode = mode;
169 + },
170 +
171 + panelTitle() {
172 + return this.mode === "visible" ? "Available skills" : "Pinned skills";
173 + },
174 +
175 + panelSubtitle() {
176 + if (this.mode === "visible") {
177 + return this.isChatMode
178 + ? "Checked skills are visible to the model in this chat. Uncheck a skill to hide its title, description, search result, and load access."
179 + : "Checked skills are visible to the model by default. Uncheck a skill to hide it from the prompt catalog and skills_tool.";
180 + }
181 + return "Check a skill to pin its full instructions into prompt extras. Uncheck it to remove the pin.";
182 + },
183 +
184 + isHidden(skill) {
185 + return this.hiddenSkills.some((entry) => entriesMatch(entry, skill));
186 + },
187 +
188 isSelected(skill) {
125 - return this.selectedSkills.some((entry) => entryKey(entry) === entryKey(skill));
189 + if (this.mode === "visible") {
190 + return !this.isHidden(skill);
191 + }
192 + return this.selectedSkills.some((entry) => entriesMatch(entry, skill));
193 },
194
195 isCheckboxDisabled(skill) {
196 + if (this.mode === "visible") {
197 + return this.mutatingChat || (this.isChatMode && !this.chatContextAvailable);
198 + }
199 return this.mutatingChat || (!this.isSelected(skill) && this.selectedCount >= this.maxActiveSkills);
200 },
201
@@ -159,7 +229,7 @@ window.createSkillsConfigModel = (context, config) => ({
229 return name ? this.catalogMap.get(name) || null : null;
230 },
231
162 - _setSelectedSkills(entries) {
232 + _setSelectedSkills(entries, { writeConfig = true } = {}) {
233 const normalized = [];
234 const seen = new Set();
235
@@ -173,12 +243,50 @@ window.createSkillsConfigModel = (context, config) => ({
243 }
244
245 this.selectedSkills = normalized;
176 - config.active_skills = compactEntries(normalized);
246 + if (writeConfig) {
247 + config.active_skills = compactEntries(normalized, this.maxActiveSkills);
248 + }
249 + },
250 +
251 + _setHiddenSkills(entries, { writeConfig = true } = {}) {
252 + const normalized = compactEntries(entries);
253 + this.hiddenSkills = normalized;
254 + if (writeConfig) {
255 + config.hidden_skills = normalized;
256 + }
257 },
258
259 async toggleSkill(skill, selected) {
180 - const key = entryKey(skill);
181 - const nextEntries = this.selectedSkills.filter((entry) => entryKey(entry) !== key);
260 + if (this.mode === "visible") {
261 + await this.toggleSkillVisibility(skill, selected);
262 + return;
263 + }
264 + await this.togglePinnedSkill(skill, selected);
265 + },
266 +
267 + async toggleSkillVisibility(skill, selected) {
268 + const previous = [...this.hiddenSkills];
269 + const nextEntries = this.hiddenSkills.filter((entry) => !entriesMatch(entry, skill));
270 +
271 + if (!selected) {
272 + nextEntries.push({
273 + name: String(skill.name || "").trim(),
274 + path: String(skill.path || "").trim(),
275 + });
276 + }
277 +
278 + this._setHiddenSkills(nextEntries, { writeConfig: !this.isChatMode });
279 +
280 + if (this.isChatMode && this.chatContextAvailable) {
281 + const ok = await this.submitChatAction(selected ? "show" : "hide", skill);
282 + if (!ok) {
283 + this._setHiddenSkills(previous, { writeConfig: false });
284 + }
285 + }
286 + },
287 +
288 + async togglePinnedSkill(skill, selected) {
289 + const nextEntries = this.selectedSkills.filter((entry) => !entriesMatch(entry, skill));
290
291 if (selected) {
292 if (this.selectedCount >= this.maxActiveSkills && !this.isSelected(skill)) {
@@ -195,9 +303,9 @@ window.createSkillsConfigModel = (context, config) => ({
303 });
304 }
305
198 - this._setSelectedSkills(nextEntries);
306 + this._setSelectedSkills(nextEntries, { writeConfig: !this.isChatMode });
307
200 - if (this.chatContextAvailable) {
308 + if (this.isChatMode && this.chatContextAvailable) {
309 await this.submitChatAction(selected ? "activate" : "deactivate", skill);
310 }
311 },
@@ -207,9 +315,20 @@ window.createSkillsConfigModel = (context, config) => ({
315 },
316
317 async clearSelections() {
318 + if (this.mode === "visible") {
319 + const previous = [...this.hiddenSkills];
320 + this._setHiddenSkills([], { writeConfig: !this.isChatMode });
321 + if (this.isChatMode && this.chatContextAvailable) {
322 + for (const entry of previous) {
323 + await this.submitChatAction("show", entry);
324 + }
325 + }
326 + return;
327 + }
328 +
329 const previous = [...this.selectedSkills];
211 - this._setSelectedSkills([]);
212 - if (this.chatContextAvailable) {
330 + this._setSelectedSkills([], { writeConfig: !this.isChatMode });
331 + if (this.isChatMode && this.chatContextAvailable) {
332 for (const entry of previous) {
333 await this.submitChatAction("deactivate", entry);
334 }
@@ -220,7 +339,17 @@ window.createSkillsConfigModel = (context, config) => ({
339 this.chatContextAvailable = !!response?.context_available;
340 const activeFromChat = Array.isArray(response?.active_skills) ? response.active_skills : null;
341 const activeFromConfig = this.activeEntries;
223 - this._setSelectedSkills(activeFromChat || activeFromConfig);
342 + const hiddenFromChat = Array.isArray(response?.hidden_skills) ? response.hidden_skills : null;
343 + const hiddenFromConfig = this.hiddenEntries;
344 +
345 + this._setSelectedSkills(
346 + this.isChatMode ? activeFromChat || [] : activeFromConfig,
347 + { writeConfig: !this.isChatMode },
348 + );
349 + this._setHiddenSkills(
350 + this.isChatMode ? hiddenFromChat || [] : hiddenFromConfig,
351 + { writeConfig: !this.isChatMode },
352 + );
353 },
354
355 async loadCatalog() {
@@ -229,7 +358,7 @@ window.createSkillsConfigModel = (context, config) => ({
358 const response = await API.callJsonApi(CATALOG_API, {
359 action: "list",
360 project_name: context.projectName || "",
232 - context_id: chatsStore.selectedContext?.id || "",
361 + context_id: this.isChatMode ? chatsStore.selectedContext?.id || "" : "",
362 });
363
364 if (!response?.ok) {
@@ -244,6 +373,7 @@ window.createSkillsConfigModel = (context, config) => ({
373 this.maxActiveSkills = MAX_ACTIVE_SKILLS_FALLBACK;
374 this.chatContextAvailable = false;
375 this._setSelectedSkills(this.activeEntries);
376 + this._setHiddenSkills(this.hiddenEntries);
377 await toastFrontendError(error?.message || "Failed to load skills", "Skills");
378 } finally {
379 this.loadingCatalog = false;
@@ -274,6 +404,7 @@ window.createSkillsConfigModel = (context, config) => ({
404 this.catalog = Array.isArray(response.skills) ? response.skills : this.catalog;
405 this.maxActiveSkills = Number(response.max_active_skills) || this.maxActiveSkills;
406 this.chatContextAvailable = !!response.context_available;
407 + this.applyCatalogState(response);
408 return true;
409 } catch (error) {
410 await toastFrontendError(error?.message || "Failed to update skills", "Skills");
plugins/_skills/webui/config.html
+73 -5
@@ -25,7 +25,29 @@
25 >
26 </label>
27
28 + <div class="skills-mode-toggle" role="tablist" aria-label="Skills mode">
29 + <button
30 + type="button"
31 + class="button"
32 + :class="{ 'is-active': mode === 'visible' }"
33 + @click="setMode('visible')"
34 + >
35 + <span class="icon material-symbols-outlined">visibility</span>
36 + Visible
37 + </button>
38 + <button
39 + type="button"
40 + class="button"
41 + :class="{ 'is-active': mode === 'pinned' }"
42 + @click="setMode('pinned')"
43 + >
44 + <span class="icon material-symbols-outlined">push_pin</span>
45 + Pinned
46 + </button>
47 + </div>
48 +
49 <div class="skills-actions">
50 + <span class="skills-count" x-text="currentCountLabel"></span>
51 <button type="button" class="button" @click="loadCatalog()" :disabled="loadingCatalog || mutatingChat">
52 <span class="icon material-symbols-outlined">refresh</span>
53 Refresh
@@ -33,8 +55,8 @@
55 </div>
56 </div>
57
36 - <div class="skills-panel" x-show="selectedSkills.length > 0">
37 - <div class="skills-panel-title">Active skills</div>
58 + <div class="skills-panel" x-show="mode === 'pinned' && selectedSkills.length > 0">
59 + <div class="skills-panel-title">Pinned skills</div>
60 <div class="skills-selected-list">
61 <template x-for="entry in selectedSkills" :key="entryKey(entry)">
62 <div class="skills-selected-card" :class="{ 'is-missing': isEntryMissing(entry) }">
@@ -69,8 +91,8 @@
91 </div>
92
93 <div class="skills-panel">
72 - <div class="skills-panel-title">Available skills</div>
73 - <div class="skills-panel-subtitle">Check a skill to add it. Uncheck it to remove it.</div>
94 + <div class="skills-panel-title" x-text="panelTitle()"></div>
95 + <div class="skills-panel-subtitle" x-text="panelSubtitle()"></div>
96
97 <div class="skills-loading" x-show="loadingCatalog">
98 <span class="material-symbols-outlined spinning">progress_activity</span>
@@ -83,7 +105,13 @@
105
106 <div class="skills-list">
107 <template x-for="skill in filteredCatalog" :key="skill.path">
86 - <label class="skills-card" :class="{ 'is-selected': isSelected(skill) }">
108 + <label
109 + class="skills-card"
110 + :class="{
111 + 'is-selected': isSelected(skill),
112 + 'is-hidden': mode === 'visible' && isHidden(skill),
113 + }"
114 + >
115 <input
116 type="checkbox"
117 :checked="isSelected(skill)"
@@ -93,6 +121,12 @@
121 <div class="skills-card-copy">
122 <div class="skills-card-title" x-text="skill.name || '(unnamed skill)'"></div>
123 <div class="skills-card-description" x-text="skill.description || 'No description provided.'"></div>
124 + <div
125 + class="skills-card-state"
126 + x-show="mode === 'visible' && isHidden(skill)"
127 + >
128 + Hidden from model
129 + </div>
130 </div>
131 <button
132 type="button"
@@ -151,6 +185,28 @@
185 margin-left: auto;
186 }
187
188 + .skills-mode-toggle {
189 + display: flex;
190 + align-items: center;
191 + gap: 0.35rem;
192 + flex: 0 0 auto;
193 + border: 1px solid var(--color-border);
194 + border-radius: 0.5rem;
195 + background: var(--color-bg-primary);
196 + padding: 0.2rem;
197 + }
198 +
199 + .skills-mode-toggle .button {
200 + border: none;
201 + background: transparent;
202 + min-height: 2rem;
203 + }
204 +
205 + .skills-mode-toggle .button.is-active {
206 + background: color-mix(in srgb, var(--color-primary) 14%, var(--color-bg-primary));
207 + color: var(--color-primary);
208 + }
209 +
210 .skills-count {
211 color: var(--color-text-secondary);
212 font-size: var(--font-size-small);
@@ -228,6 +284,11 @@
284 background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-primary));
285 }
286
287 + .skills-card.is-hidden {
288 + border-style: dashed;
289 + background: color-mix(in srgb, var(--color-bg-secondary) 55%, var(--color-bg-primary));
290 + }
291 +
292 .skills-card input {
293 margin-top: 0.15rem;
294 }
@@ -250,6 +311,13 @@
311 line-height: 1.45;
312 }
313
314 + .skills-card-state {
315 + margin-top: 0.4rem;
316 + color: var(--color-text-secondary);
317 + font-size: var(--font-size-small);
318 + font-weight: 600;
319 + }
320 +
321 .skills-loading {
322 display: flex;
323 align-items: center;
tests/test_skills_runtime.py
+73
@@ -117,6 +117,15 @@ def test_active_skills_cap_is_twenty():
117 assert runtime.get_max_active_skills() == 20
118
119
120 +def test_hidden_skills_are_not_capped_like_active_skills():
121 + agent = DummyAgent()
122 + entries = [{"name": f"Hidden {index}"} for index in range(25)]
123 + agent.context.set_data(runtime.CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS, entries)
124 +
125 + assert len(runtime.get_chat_disabled_skills(agent.context)) == 25
126 + assert len(runtime.get_hidden_skills(agent)) == 25
127 +
128 +
129 def test_chat_activation_can_override_scope_defaults(monkeypatch):
130 monkeypatch.setattr(
131 runtime.plugin_helpers,
@@ -336,6 +345,7 @@ def test_clearing_chat_overrides_restores_scope_defaults(monkeypatch):
345 ]
346 assert runtime.get_chat_active_skills(agent.context) == []
347 assert runtime.get_chat_disabled_skills(agent.context) == []
348 + assert runtime.get_chat_visible_skills(agent.context) == []
349
350
351 def test_activating_new_skill_fails_once_limit_is_full(monkeypatch):
@@ -352,3 +362,66 @@ def test_activating_new_skill_fails_once_limit_is_full(monkeypatch):
362 runtime.activate_chat_skill(agent, {"name": "Overflow"})
363
364 assert len(runtime.get_active_skills(agent)) == 20
365 +
366 +
367 +def test_hidden_skills_filter_agent_visible_skill_catalog(monkeypatch, tmp_path: Path):
368 + skills_root = tmp_path / "skills"
369 + for name in ("alpha-skill", "beta-skill"):
370 + skill_dir = skills_root / name
371 + skill_dir.mkdir(parents=True)
372 + (skill_dir / "SKILL.md").write_text(
373 + f"---\nname: {name}\ndescription: {name} description\n---\nBody\n",
374 + encoding="utf-8",
375 + )
376 +
377 + monkeypatch.setattr(
378 + runtime.subagents,
379 + "get_paths",
380 + lambda agent, *parts: [str(skills_root)],
381 + )
382 + monkeypatch.setattr(runtime.files, "exists", lambda path: str(path) == str(skills_root))
383 + monkeypatch.setattr(
384 + runtime.plugin_helpers,
385 + "get_plugin_config",
386 + lambda *args, **kwargs: {"hidden_skills": [{"name": "beta-skill"}]},
387 + )
388 +
389 + agent = DummyAgent()
390 +
391 + assert [skill.name for skill in runtime.list_skills(agent)] == ["alpha-skill"]
392 + assert [skill.name for skill in runtime.list_skills(agent, include_hidden=True)] == [
393 + "alpha-skill",
394 + "beta-skill",
395 + ]
396 + assert runtime.search_skills("beta", agent=agent) == []
397 + assert [skill.name for skill in runtime.search_skills("beta", agent=agent, include_hidden=True)] == [
398 + "beta-skill"
399 + ]
400 + assert runtime.find_skill("beta-skill", agent=agent) is None
401 + assert runtime.find_skill("beta-skill", agent=agent, include_hidden=True).name == "beta-skill"
402 +
403 + catalog = runtime.list_skill_catalog(agent=agent)
404 + hidden_by_name = {item["name"]: item["hidden"] for item in catalog}
405 + assert hidden_by_name == {
406 + "alpha-skill": False,
407 + "beta-skill": True,
408 + }
409 +
410 +
411 +def test_chat_visible_override_restores_scope_hidden_skill(monkeypatch):
412 + monkeypatch.setattr(
413 + runtime.plugin_helpers,
414 + "get_plugin_config",
415 + lambda *args, **kwargs: {"hidden_skills": [{"name": "beta-skill"}]},
416 + )
417 + agent = DummyAgent()
418 +
419 + assert runtime.get_hidden_skills(agent) == [{"name": "beta-skill"}]
420 +
421 + runtime.show_chat_skill(agent, {"name": "beta-skill"})
422 + assert runtime.get_hidden_skills(agent) == []
423 + assert runtime.get_chat_visible_skills(agent.context) == [{"name": "beta-skill"}]
424 +
425 + runtime.hide_chat_skill(agent, {"name": "beta-skill"})
426 + assert runtime.get_hidden_skills(agent) == [{"name": "beta-skill"}]
427 + assert runtime.get_chat_visible_skills(agent.context) == []
webui/components/plugins/plugin-settings-store.js
+18 -3
@@ -24,6 +24,7 @@ const model = {
24 settingsSnapshotJson: "",
25 previousProjectName: "",
26 previousAgentProfileKey: "",
27 + openOptions: {},
28
29 _toComparableJson(value) {
30 try {
@@ -47,9 +48,17 @@ const model = {
48 },
49
50 get modalTitle() {
51 + if (this.openOptions?.title) return this.openOptions.title;
52 + if (this.openOptions?.focus === "chat" && this.pluginName === "_skills") {
53 + return "Skills";
54 + }
55 return `${this.pluginTitle} Settings`;
56 },
57
58 + get hideSettingsActions() {
59 + return !!this.openOptions?.hideSettingsActions || this.openOptions?.focus === "chat";
60 + },
61 +
62 confirmDiscardUnsavedChanges() {
63 if (!this.hasUnsavedChanges) return true;
64 return window.confirm("You have unsaved changes that will be lost. Continue?");
@@ -89,12 +98,17 @@ const model = {
98 };
99 },
100
92 - _applyPluginState(pluginMeta, { projectName = "", agentProfileKey = "" } = {}) {
101 + _applyPluginState(
102 + pluginMeta,
103 + { projectName = "", agentProfileKey = "" } = {},
104 + openOptions = {},
105 + ) {
106 this.pluginName = pluginMeta?.name || null;
107 this.pluginMeta = pluginMeta || null;
108 this.settings = {};
109 this.settingsSnapshotJson = "";
110 this.wizardFooter = null;
111 + this.openOptions = openOptions && typeof openOptions === "object" ? openOptions : {};
112 this.error = null;
113 this.projectName = projectName;
114 this.agentProfileKey = agentProfileKey;
@@ -256,7 +270,7 @@ const model = {
270 isSaving: false,
271 error: null,
272
259 - async openConfig(pluginName, projectName = "", agentProfile = "") {
273 + async openConfig(pluginName, projectName = "", agentProfile = "", openOptions = {}) {
274 if (!pluginName) {
275 throw new Error("Missing plugin name.");
276 }
@@ -272,7 +286,7 @@ const model = {
286
287 await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
288 const resolvedScope = this._resolveScope(pluginMeta, projectName || "", agentProfile || "");
275 - this._applyPluginState(pluginMeta, resolvedScope);
289 + this._applyPluginState(pluginMeta, resolvedScope, openOptions);
290 await this.loadSettings();
291
292 if (!pluginToggleStore?.open) {
@@ -401,6 +415,7 @@ const model = {
415 this.agentProfileKey = "";
416 this.settings = {};
417 this.settingsSnapshotJson = "";
418 + this.openOptions = {};
419 this.wizardFooter = null;
420 this.previousProjectName = "";
421 this.previousAgentProfileKey = "";
webui/components/plugins/plugin-settings.html
+3 -2
@@ -29,7 +29,7 @@
29
30 <!-- Context toolbar: Project + Agent profile (only when at least one scope is configurable) -->
31 <div class="plugin-settings-scope-section"
32 - x-show="context.perProjectConfig || context.perAgentConfig">
32 + x-show="(context.perProjectConfig || context.perAgentConfig) && !context.hideSettingsActions">
33 <div class="plugin-settings-scope-header">
34 <div class="plugin-settings-scope-header-copy">
35 <div class="plugin-settings-scope-title">Settings scope</div>
@@ -136,6 +136,7 @@
136
137 <div class="plugin-settings-footer-actions">
138 <button class="btn"
139 + x-show="!context.hideSettingsActions"
140 @click="context.resetToDefault()"
141 :disabled="context?.isSaving || context?.isLoading">
142 Default
@@ -147,7 +148,7 @@
148 <span x-text="context.wizardFooter?.nextLabel?.() || 'Next'"></span>
149 </button>
150 <button class="btn btn-ok"
150 - x-show="!context.wizardFooter || context.wizardFooter?.showSave?.()"
151 + x-show="!context.hideSettingsActions && (!context.wizardFooter || context.wizardFooter?.showSave?.())"
152 @click="context.save()"
153 :disabled="context?.isSaving || context?.isLoading">
154 Save