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 {