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