Simplify skills selector layout
- replace the Visible/Pinned mode switch with a single continuous skills view - show pinned skills first, then all skills below a divider with line-based rows instead of boxed cards - keep pinning on the checkbox and move hide/show into an inline eye control for each skill
Alessandro committed
Jun 1, 2026 at 17:06 UTC
34d23ddc3ec3935be9c8fddcd7365b2d9eb5023f
2 files changed
+140
-189
plugins/_skills/webui/config-store.js
+30
-40
@@ -94,7 +94,6 @@ window.createSkillsConfigModel = (context, config) => ({
94
mutatingChat: false,
95
catalog: [],
96
search: "",
97
- mode: "pinned",
97
maxActiveSkills: MAX_ACTIVE_SKILLS_FALLBACK,
98
selectedSkills: [],
99
hiddenSkills: [],
@@ -103,7 +102,6 @@ window.createSkillsConfigModel = (context, config) => ({
102
initDefaults() {
103
ensureConfig(config);
104
this.maxActiveSkills = config.max_active_skills;
106
- this.mode = this.isChatMode ? "visible" : "pinned";
105
this.selectedSkills = [...this.activeEntries];
106
this.hiddenSkills = [...this.hiddenEntries];
107
},
@@ -189,22 +187,31 @@ window.createSkillsConfigModel = (context, config) => ({
187
return entryKey(entry);
188
},
189
192
- setMode(mode) {
193
- if (!["visible", "pinned"].includes(mode)) return;
194
- this.mode = mode;
190
+ pinnedSubtitle() {
191
+ return this.isChatMode
192
+ ? "These skills are currently pinned into the prompt for this chat."
193
+ : "These skills are pinned by default in this scope.";
194
},
195
197
- panelTitle() {
198
- return this.mode === "visible" ? "Available skills" : "Pinned skills";
196
+ allSkillsSubtitle() {
197
+ return this.isChatMode
198
+ ? "Check a skill to pin it for this chat. Use the eye control to hide or show it in this chat."
199
+ : "Check a skill to pin it by default. Use the eye control to hide or show it in this scope.";
200
},
201
201
- panelSubtitle() {
202
- if (this.mode === "visible") {
203
- return this.isChatMode
204
- ? "Checked skills are visible to the model in this chat. Uncheck a skill to hide its title, description, search result, and load access."
205
- : "Checked skills are visible to the model by default. Uncheck a skill to hide it from the prompt catalog and skills_tool.";
202
+ hiddenStateLabel() {
203
+ return this.isChatMode ? "Hidden in this chat" : "Hidden by default";
204
+ },
205
+
206
+ visibilityButtonTitle(skill) {
207
+ if (this.isHidden(skill)) {
208
+ return this.isChatMode ? "Show in this chat" : "Show by default";
209
}
207
- return "Check a skill to pin its full instructions into prompt extras. Uncheck it to remove the pin.";
210
+ return this.isChatMode ? "Hide in this chat" : "Hide by default";
211
+ },
212
+
213
+ visibilityButtonIcon(skill) {
214
+ return this.isHidden(skill) ? "visibility_off" : "visibility";
215
},
216
217
isHidden(skill) {
@@ -212,19 +219,17 @@ window.createSkillsConfigModel = (context, config) => ({
219
},
220
221
isSelected(skill) {
215
- if (this.mode === "visible") {
216
- return !this.isHidden(skill);
217
- }
222
return this.selectedSkills.some((entry) => entriesMatch(entry, skill));
223
},
224
221
- isCheckboxDisabled(skill) {
222
- if (this.mode === "visible") {
223
- return this.mutatingChat || (this.isChatMode && !this.chatContextAvailable);
224
- }
225
+ isPinDisabled(skill) {
226
return this.mutatingChat || (!this.isSelected(skill) && this.selectedCount >= this.maxActiveSkills);
227
},
228
229
+ isVisibilityDisabled() {
230
+ return this.mutatingChat || (this.isChatMode && !this.chatContextAvailable);
231
+ },
232
+
233
isEntryMissing(entry) {
234
const key = entryKey(entry);
235
if (!key) return false;
@@ -282,14 +287,6 @@ window.createSkillsConfigModel = (context, config) => ({
287
}
288
},
289
285
- async toggleSkill(skill, selected) {
286
- if (this.mode === "visible") {
287
- await this.toggleSkillVisibility(skill, selected);
288
- return;
289
- }
290
- await this.togglePinnedSkill(skill, selected);
291
- },
292
-
290
async toggleSkillVisibility(skill, selected) {
291
const previous = [...this.hiddenSkills];
292
const nextEntries = this.hiddenSkills.filter((entry) => !entriesMatch(entry, skill));
@@ -311,6 +308,10 @@ window.createSkillsConfigModel = (context, config) => ({
308
}
309
},
310
311
+ async toggleVisibility(skill) {
312
+ await this.toggleSkillVisibility(skill, this.isHidden(skill));
313
+ },
314
+
315
async togglePinnedSkill(skill, selected) {
316
const nextEntries = this.selectedSkills.filter((entry) => !entriesMatch(entry, skill));
317
@@ -337,21 +338,10 @@ window.createSkillsConfigModel = (context, config) => ({
338
},
339
340
async removeEntry(entry) {
340
- await this.toggleSkill(entry, false);
341
+ await this.togglePinnedSkill(entry, false);
342
},
343
344
async clearSelections() {
344
- if (this.mode === "visible") {
345
- const previous = [...this.hiddenSkills];
346
- this._setHiddenSkills([], { writeConfig: !this.isChatMode });
347
- if (this.isChatMode && this.chatContextAvailable) {
348
- for (const entry of previous) {
349
- await this.submitChatAction("show", entry);
350
- }
351
- }
352
- return;
353
- }
354
-
345
const previous = [...this.selectedSkills];
346
this._setSelectedSkills([], { writeConfig: !this.isChatMode });
347
if (this.isChatMode && this.chatContextAvailable) {
plugins/_skills/webui/config.html
+110
-149
@@ -45,28 +45,6 @@
45
>
46
</label>
47
48
- <div class="skills-mode-toggle" role="tablist" aria-label="Skills mode">
49
- <button
50
- type="button"
51
- class="skills-mode-button"
52
- :class="{ 'is-active': mode === 'visible' }"
53
- @click="setMode('visible')"
54
- >
55
- <span class="icon material-symbols-outlined">visibility</span>
56
- Visible
57
- </button>
58
- <span class="skills-mode-separator" aria-hidden="true">/</span>
59
- <button
60
- type="button"
61
- class="skills-mode-button"
62
- :class="{ 'is-active': mode === 'pinned' }"
63
- @click="setMode('pinned')"
64
- >
65
- <span class="icon material-symbols-outlined">push_pin</span>
66
- Pinned
67
- </button>
68
- </div>
69
-
48
<div class="skills-actions">
49
<button type="button" class="button" @click="loadCatalog()" :disabled="loadingCatalog || mutatingChat">
50
<span class="icon material-symbols-outlined">refresh</span>
@@ -75,16 +53,22 @@
53
</div>
54
</div>
55
78
- <div class="skills-panel" x-show="mode === 'pinned' && selectedSkills.length > 0">
56
+ <div class="skills-section">
57
<div class="skills-panel-title">Pinned skills</div>
58
+ <div class="skills-panel-subtitle" x-text="pinnedSubtitle()"></div>
59
+
60
+ <template x-if="!loadingCatalog && selectedSkills.length === 0">
61
+ <div class="skills-empty">No pinned skills yet. Pin from the list below.</div>
62
+ </template>
63
+
64
<div class="skills-selected-list">
65
<template x-for="entry in selectedSkills" :key="entryKey(entry)">
82
- <div class="skills-selected-card" :class="{ 'is-missing': isEntryMissing(entry) }">
83
- <div class="skills-selected-copy">
84
- <div class="skills-selected-title" x-text="labelForEntry(entry)"></div>
85
- <div class="skills-selected-meta" x-text="secondaryLabelForEntry(entry)"></div>
66
+ <div class="skills-row skills-pinned-row" :class="{ 'is-missing': isEntryMissing(entry) }">
67
+ <div class="skills-row-copy">
68
+ <div class="skills-row-title" x-text="labelForEntry(entry)"></div>
69
+ <div class="skills-row-meta" x-text="secondaryLabelForEntry(entry)"></div>
70
</div>
87
- <div class="skills-card-actions">
71
+ <div class="skills-row-actions">
72
<button
73
type="button"
74
class="button icon-button"
@@ -110,9 +94,11 @@
94
</div>
95
</div>
96
113
- <div class="skills-panel">
114
- <div class="skills-panel-title" x-text="panelTitle()"></div>
115
- <div class="skills-panel-subtitle" x-text="panelSubtitle()"></div>
97
+ <hr class="skills-divider">
98
+
99
+ <div class="skills-section">
100
+ <div class="skills-panel-title">All skills</div>
101
+ <div class="skills-panel-subtitle" x-text="allSkillsSubtitle()"></div>
102
103
<div class="skills-loading" x-show="loadingCatalog">
104
<span class="material-symbols-outlined spinning">progress_activity</span>
@@ -125,39 +111,54 @@
111
112
<div class="skills-list">
113
<template x-for="skill in filteredCatalog" :key="skill.path">
128
- <label
129
- class="skills-card"
114
+ <div
115
+ class="skills-row skills-catalog-row"
116
:class="{
131
- 'is-selected': isSelected(skill),
132
- 'is-hidden': mode === 'visible' && isHidden(skill),
117
+ 'is-pinned': isSelected(skill),
118
+ 'is-hidden': isHidden(skill),
119
}"
120
>
135
- <input
136
- type="checkbox"
137
- :checked="isSelected(skill)"
138
- :disabled="isCheckboxDisabled(skill)"
139
- @change="toggleSkill(skill, $event.target.checked)"
140
- >
141
- <div class="skills-card-copy">
142
- <div class="skills-card-title" x-text="skill.name || '(unnamed skill)'"></div>
143
- <div class="skills-card-description" x-text="skill.description || 'No description provided.'"></div>
121
+ <label class="skills-checkbox" :title="isSelected(skill) ? 'Pinned' : 'Pin skill'">
122
+ <input
123
+ type="checkbox"
124
+ :checked="isSelected(skill)"
125
+ :disabled="isPinDisabled(skill)"
126
+ @change="togglePinnedSkill(skill, $event.target.checked)"
127
+ >
128
+ </label>
129
+ <div class="skills-row-copy">
130
+ <div class="skills-row-title" x-text="skill.name || '(unnamed skill)'"></div>
131
+ <div class="skills-row-description" x-text="skill.description || 'No description provided.'"></div>
132
<div
145
- class="skills-card-state"
146
- x-show="mode === 'visible' && isHidden(skill)"
133
+ class="skills-row-state"
134
+ x-show="isHidden(skill)"
135
+ x-text="hiddenStateLabel()"
136
>
148
- Hidden from model
137
</div>
138
</div>
151
- <button
152
- type="button"
153
- class="button icon-button"
154
- title="Open skill"
155
- aria-label="Open skill"
156
- @click.prevent.stop="openSkill(skill)"
157
- >
158
- <span class="icon material-symbols-outlined">article</span>
159
- </button>
160
- </label>
139
+ <div class="skills-row-actions">
140
+ <button
141
+ type="button"
142
+ class="button icon-button skills-visibility-button"
143
+ :class="{ 'is-hidden-state': isHidden(skill) }"
144
+ :title="visibilityButtonTitle(skill)"
145
+ :aria-label="visibilityButtonTitle(skill)"
146
+ :disabled="isVisibilityDisabled()"
147
+ @click.prevent.stop="toggleVisibility(skill)"
148
+ >
149
+ <span class="icon material-symbols-outlined" x-text="visibilityButtonIcon(skill)"></span>
150
+ </button>
151
+ <button
152
+ type="button"
153
+ class="button icon-button"
154
+ title="Open skill"
155
+ aria-label="Open skill"
156
+ @click.prevent.stop="openSkill(skill)"
157
+ >
158
+ <span class="icon material-symbols-outlined">article</span>
159
+ </button>
160
+ </div>
161
+ </div>
162
</template>
163
</div>
164
</div>
@@ -205,53 +206,22 @@
206
margin-left: auto;
207
}
208
208
- .skills-mode-toggle {
209
+ .skills-section {
210
display: flex;
210
- align-items: center;
211
- gap: 0.45rem;
212
- flex: 0 0 auto;
213
- }
214
-
215
- .skills-mode-button {
216
- display: inline-flex;
217
- align-items: center;
218
- gap: 0.35rem;
219
- padding: 0;
220
- border: none;
221
- background: transparent;
222
- min-height: 0;
223
- color: var(--color-text-secondary);
224
- font: inherit;
225
- font-weight: 650;
226
- cursor: pointer;
227
- transition: color 0.15s ease, opacity 0.15s ease;
228
- }
229
-
230
- .skills-mode-button:hover {
231
- color: var(--color-text);
232
- filter: none;
233
- box-shadow: none;
234
- }
235
-
236
- .skills-mode-button.is-active {
237
- color: var(--color-text);
238
- }
239
-
240
- .skills-mode-button:focus-visible {
241
- outline: none;
242
- color: var(--color-text);
211
+ flex-direction: column;
212
}
213
245
- .skills-mode-separator {
246
- color: var(--color-text-secondary);
247
- font-weight: 500;
248
- user-select: none;
214
+ .skills-divider {
215
+ width: 100%;
216
+ border: 0;
217
+ border-top: 1px solid var(--color-border);
218
+ margin: 0;
219
}
220
221
.skills-help,
222
.skills-panel-subtitle,
253
- .skills-selected-meta,
254
- .skills-card-description {
223
+ .skills-row-meta,
224
+ .skills-row-description {
225
color: var(--color-text-secondary);
226
font-size: var(--font-size-small);
227
}
@@ -265,74 +235,50 @@
235
.skills-list {
236
display: flex;
237
flex-direction: column;
268
- gap: 0.65rem;
238
margin-top: 0.75rem;
239
}
240
272
- .skills-selected-card,
273
- .skills-card {
274
- border: 1px solid var(--color-border);
275
- border-radius: 0.75rem;
276
- background: var(--color-bg-primary);
241
+ .skills-row {
242
+ display: flex;
243
+ align-items: flex-start;
244
+ gap: 0.85rem;
245
+ padding: 0.95rem 0;
246
}
247
279
- .skills-selected-card {
280
- display: flex;
281
- align-items: center;
282
- justify-content: space-between;
283
- gap: 0.75rem;
284
- padding: 0.8rem 0.9rem;
248
+ .skills-row + .skills-row {
249
+ border-top: 1px solid var(--color-border);
250
+ }
251
+
252
+ .skills-row.is-pinned .skills-row-title {
253
+ color: var(--color-text);
254
}
255
287
- .skills-selected-card.is-missing {
288
- border-style: dashed;
256
+ .skills-row.is-hidden {
257
+ opacity: 0.72;
258
}
259
291
- .skills-selected-copy {
260
+ .skills-row.is-missing .skills-row-title::after {
261
+ content: " (missing)";
262
+ color: var(--color-text-secondary);
263
+ font-weight: 500;
264
+ }
265
+
266
+ .skills-row-copy {
267
flex: 1 1 auto;
268
min-width: 0;
269
}
270
296
- .skills-selected-title,
297
- .skills-card-title {
271
+ .skills-row-title {
272
font-weight: 650;
273
word-break: break-word;
274
}
275
302
- .skills-selected-meta {
276
+ .skills-row-meta {
277
margin-top: 0.25rem;
278
word-break: break-all;
279
}
280
307
- .skills-card {
308
- display: flex;
309
- align-items: flex-start;
310
- gap: 0.85rem;
311
- padding: 0.85rem;
312
- cursor: pointer;
313
- transition: border-color 0.15s ease, background-color 0.15s ease;
314
- }
315
-
316
- .skills-card.is-selected {
317
- border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
318
- background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-primary));
319
- }
320
-
321
- .skills-card.is-hidden {
322
- border-style: dashed;
323
- background: color-mix(in srgb, var(--color-bg-secondary) 55%, var(--color-bg-primary));
324
- }
325
-
326
- .skills-card input {
327
- margin-top: 0.15rem;
328
- }
329
-
330
- .skills-card-copy {
331
- flex: 1 1 auto;
332
- min-width: 0;
333
- }
334
-
335
- .skills-card-actions {
281
+ .skills-row-actions {
282
display: flex;
283
align-items: center;
284
gap: 0.5rem;
@@ -340,18 +286,35 @@
286
margin-left: auto;
287
}
288
343
- .skills-card-description {
289
+ .skills-row-description {
290
margin-top: 0.3rem;
291
line-height: 1.45;
292
}
293
348
- .skills-card-state {
294
+ .skills-row-state {
295
margin-top: 0.4rem;
296
color: var(--color-text-secondary);
297
font-size: var(--font-size-small);
298
font-weight: 600;
299
}
300
301
+ .skills-checkbox {
302
+ display: inline-flex;
303
+ align-items: flex-start;
304
+ justify-content: center;
305
+ width: 1.25rem;
306
+ margin-top: 0.1rem;
307
+ }
308
+
309
+ .skills-checkbox input {
310
+ margin: 0.1rem 0 0;
311
+ accent-color: var(--color-primary);
312
+ }
313
+
314
+ .skills-visibility-button.is-hidden-state {
315
+ color: var(--color-text-secondary);
316
+ }
317
+
318
.skills-loading {
319
display: flex;
320
align-items: center;
@@ -361,11 +324,9 @@
324
}
325
326
.skills-empty {
364
- border: 1px dashed var(--color-border);
365
- border-radius: 0.75rem;
327
color: var(--color-text-secondary);
328
margin-top: 0.75rem;
368
- padding: 1rem;
329
+ padding: 0.15rem 0 0;
330
}
331
332
.button, .button.cancel {