Polish skills settings scope and search

Remove agent profile scoping from the List Skills and Import Skills settings sections so skills are scoped only by project. Move Scan Skills after Import Skills in both navigation metadata and rendered section order, and add an MCP-style search affordance to List Skills with filtered results and an empty search state.

Alessandro committed Jun 18, 2026 at 13:14 UTC 352320ef8167f1bab403860030d196e0cdf83b77
7 files changed +183 -70
tests/test_skills_scan.py
+80
@@ -126,6 +126,12 @@ def test_settings_skills_scan_section_and_prompt_assets_are_present():
126 assert "section-skills-scan" in settings_store
127 assert "settings/skills/scan.html" in skills_settings
128 assert "settings/skills/import.html" in skills_settings
129 + assert settings_store.index("section-skills-import") < settings_store.index(
130 + "section-skills-scan"
131 + )
132 + assert skills_settings.index("section-skills-import") < skills_settings.index(
133 + "section-skills-scan"
134 + )
135 assert "scanSelectedFile()" in import_template
136 assert "snyk-agent-scan@latest --json --no-bootstrap" in scan_prompt
137 assert "E004" in scan_checks_text
@@ -133,3 +139,77 @@ def test_settings_skills_scan_section_and_prompt_assets_are_present():
139 assert "W008" in scan_checks_text
140 assert "W011" in scan_checks_text
141 assert "W012" in scan_checks_text
142 +
143 +
144 +def test_list_and_import_skills_filters_are_project_only():
145 + list_template = (
146 + PROJECT_ROOT
147 + / "webui"
148 + / "components"
149 + / "settings"
150 + / "skills"
151 + / "list.html"
152 + ).read_text(encoding="utf-8")
153 + list_store = (
154 + PROJECT_ROOT
155 + / "webui"
156 + / "components"
157 + / "settings"
158 + / "skills"
159 + / "skills-list-store.js"
160 + ).read_text(encoding="utf-8")
161 + import_template = (
162 + PROJECT_ROOT
163 + / "webui"
164 + / "components"
165 + / "settings"
166 + / "skills"
167 + / "import.html"
168 + ).read_text(encoding="utf-8")
169 + import_store = (
170 + PROJECT_ROOT
171 + / "webui"
172 + / "components"
173 + / "settings"
174 + / "skills"
175 + / "skills-import-store.js"
176 + ).read_text(encoding="utf-8")
177 +
178 + assert "Agent profile" not in list_template
179 + assert "agentProfile" not in list_store
180 + assert "agent_profile" not in list_store
181 + assert "Project" in list_template
182 + assert "project_name" in list_store
183 + assert "Limit to agent profile" not in import_template
184 + assert "agentProfile" not in import_store
185 + assert "agent_profile" not in import_store
186 + assert "Limit to project" in import_template
187 + assert "project_name" in import_store
188 +
189 +
190 +def test_list_skills_has_mcp_style_search():
191 + list_template = (
192 + PROJECT_ROOT
193 + / "webui"
194 + / "components"
195 + / "settings"
196 + / "skills"
197 + / "list.html"
198 + ).read_text(encoding="utf-8")
199 + list_store = (
200 + PROJECT_ROOT
201 + / "webui"
202 + / "components"
203 + / "settings"
204 + / "skills"
205 + / "skills-list-store.js"
206 + ).read_text(encoding="utf-8")
207 +
208 + assert 'type="search"' in list_template
209 + assert "placeholder=\"Search skills\"" in list_template
210 + assert "skills-search-clear" in list_template
211 + assert "filteredSkills" in list_template
212 + assert "No skills match this search." in list_template
213 + assert "skillSearch" in list_store
214 + assert "matchesSearchQuery" in list_store
215 + assert "clearSkillSearch()" in list_store
webui/components/settings/settings-store.js
+1 -1
@@ -34,8 +34,8 @@ const TAB_ITEMS = Object.freeze([
34 icon: "school",
35 sections: [
36 { id: "section-skills-list", label: "List Skills", icon: "view_list" },
37 - { id: "section-skills-scan", label: "Scan Skills", icon: "radar" },
37 { id: "section-skills-import", label: "Import Skills", icon: "upload_file" },
38 + { id: "section-skills-scan", label: "Scan Skills", icon: "radar" },
39 ],
40 },
41 {
webui/components/settings/skills/import.html
-11
@@ -38,17 +38,6 @@
38 </select>
39 </label>
40
41 - <label class="policy-label">
42 - <span class="policy-label-text">Limit to agent profile:</span>
43 - <select x-model="$store.skillsImportStore.agentProfileKey" class="policy-dropdown"
44 - @change="$store.skillsImportStore.previewImport()">
45 - <option value="">All</option>
46 - <template x-for="agentProfile in $store.skillsImportStore.agentProfiles" :key="agentProfile.key">
47 - <option :value="agentProfile.key" x-text="agentProfile.label"></option>
48 - </template>
49 - </select>
50 - </label>
51 -
41 <label class="policy-label">
42 <span class="policy-label-text">Namespace:</span>
43 <input class="text-input" type="text" placeholder="e.g. my-pack"
webui/components/settings/skills/list.html
+65 -10
@@ -28,15 +28,16 @@
28 </select>
29 </label>
30
31 - <label class="skills-toolbar-item">
32 - <span class="skills-toolbar-label">Agent profile</span>
33 - <select x-model="$store.skillsListStore.agentProfileKey"
34 - @change="$store.skillsListStore.loadSkills()">
35 - <option value="">All</option>
36 - <template x-for="agentProfile in $store.skillsListStore.agentProfiles" :key="agentProfile.key">
37 - <option :value="agentProfile.key" x-text="agentProfile.label"></option>
38 - </template>
39 - </select>
31 + <label class="skills-search">
32 + <span class="material-symbols-outlined" aria-hidden="true">search</span>
33 + <input type="search" x-model.debounce.150ms="$store.skillsListStore.skillSearch"
34 + placeholder="Search skills" />
35 + <button type="button" class="skills-search-clear"
36 + x-show="$store.skillsListStore.skillSearchActive"
37 + @click="$store.skillsListStore.clearSkillSearch()"
38 + title="Clear search">
39 + <span class="material-symbols-outlined" aria-hidden="true">close</span>
40 + </button>
41 </label>
42
43 <div class="skills-toolbar-actions">
@@ -62,8 +63,13 @@
63 <div class="skills-empty">No skills found.</div>
64 </template>
65
66 + <template
67 + x-if="!$store.skillsListStore.loading && !$store.skillsListStore.error && $store.skillsListStore.skills.length > 0 && $store.skillsListStore.filteredSkills.length === 0">
68 + <div class="skills-empty">No skills match this search.</div>
69 + </template>
70 +
71 <div class="skills-list">
66 - <template x-for="skill in $store.skillsListStore.skills" :key="skill.path">
72 + <template x-for="skill in $store.skillsListStore.filteredSkills" :key="skill.path">
73 <div class="skill-card">
74 <div class="skill-header">
75 <div class="skill-heading">
@@ -130,6 +136,50 @@
136 flex: 0 0 auto;
137 }
138
139 + .skills-search {
140 + display: inline-flex;
141 + align-items: center;
142 + gap: 0.35rem;
143 + min-height: 2.1rem;
144 + flex: 1 1 18rem;
145 + min-width: min(18rem, 100%);
146 + padding: 0.25rem 0.45rem;
147 + border: 1px solid var(--color-border);
148 + border-radius: 7px;
149 + background: var(--color-input);
150 + color: var(--color-text-muted);
151 + }
152 +
153 + .skills-search input {
154 + width: 100%;
155 + min-width: 8rem;
156 + border: 0;
157 + background: transparent;
158 + color: var(--color-text);
159 + outline: none;
160 + padding: 0.2rem;
161 + }
162 +
163 + .skills-search input::-webkit-search-cancel-button,
164 + .skills-search input::-webkit-search-decoration {
165 + -webkit-appearance: none;
166 + appearance: none;
167 + display: none;
168 + }
169 +
170 + .skills-search-clear {
171 + display: inline-flex;
172 + align-items: center;
173 + justify-content: center;
174 + width: 1.45rem;
175 + height: 1.45rem;
176 + border: 0;
177 + border-radius: 5px;
178 + background: transparent;
179 + color: var(--color-text-muted);
180 + cursor: pointer;
181 + }
182 +
183 @media (max-width: 640px) {
184 .skills-toolbar-row {
185 gap: 0.5rem;
@@ -140,6 +190,11 @@
190 min-width: 0;
191 }
192
193 + .skills-search {
194 + flex-basis: 100%;
195 + min-width: 0;
196 + }
197 +
198 .skills-toolbar-actions {
199 width: 100%;
200 margin-left: 0;
webui/components/settings/skills/skills-import-store.js
-17
@@ -19,9 +19,7 @@ const model = {
19 namespace: "",
20 conflict: "skip", // skip|overwrite|rename
21 projectKey: "", // selected project key, empty means All
22 - agentProfileKey: "", // selected agent profile key, empty means All
22 projects: [], // available projects options [{key,label}]
24 - agentProfiles: [], // available agent profile options [{key,label}]
23
24 preview: null,
25 result: null,
@@ -29,7 +27,6 @@ const model = {
27 init() {
28 this.resetState();
29 this.loadProjects();
32 - this.loadAgentProfiles();
30 },
31
32 resetState() {
@@ -46,7 +43,6 @@ const model = {
43 this.namespace = "";
44 this.conflict = "skip";
45 this.projectKey = "";
49 - this.agentProfileKey = "";
46 },
47
48 async loadProjects() {
@@ -59,16 +55,6 @@ const model = {
55 }
56 },
57
62 - async loadAgentProfiles() {
63 - try {
64 - const data = await api.callJsonApi("/agents", { action: "list" });
65 - this.agentProfiles = data.ok ? (data.data || []) : [];
66 - } catch (e) {
67 - console.error("Failed to load agent profiles:", e);
68 - this.agentProfiles = [];
69 - }
70 - },
71 -
58 async handleFileUpload(event) {
59 const file = event.target.files[0];
60 if (!file) return;
@@ -100,9 +86,6 @@ const model = {
86 formData.append("project_name", this.projectKey);
87 }
88
103 - if (this.agentProfileKey) {
104 - formData.append("agent_profile", this.agentProfileKey);
105 - }
89 return formData;
90 },
91
webui/components/settings/skills/skills-list-store.js
+27 -21
@@ -3,18 +3,23 @@ import { store as fileBrowserStore } from "/components/modals/file-browser/file-
3
4 const fetchApi = globalThis.fetchApi;
5
6 +function matchesSearchQuery(query, values) {
7 + const normalized = String(query || "").trim().toLowerCase();
8 + if (!normalized) return true;
9 + return values.some((value) => String(value ?? "").toLowerCase().includes(normalized));
10 +}
11 +
12 const model = {
13 loading: false,
14 error: "",
15 skills: [],
16 projects: [],
17 projectName: "",
12 - agentProfiles: [],
13 - agentProfileKey: "",
18 + skillSearch: "",
19
20 async init() {
21 this.resetState();
17 - await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
22 + await this.loadProjects();
23 await this.loadSkills();
24 },
25
@@ -24,29 +29,13 @@ const model = {
29 this.skills = [];
30 this.projects = [];
31 this.projectName = "";
27 - this.agentProfiles = [];
28 - this.agentProfileKey = "";
32 + this.skillSearch = "";
33 },
34
35 onClose() {
36 this.resetState();
37 },
38
35 - async loadAgentProfiles() {
36 - try {
37 - const response = await fetchApi("/agents", {
38 - method: "POST",
39 - headers: { "Content-Type": "application/json" },
40 - body: JSON.stringify({ action: "list" }),
41 - });
42 - const data = await response.json().catch(() => ({}));
43 - this.agentProfiles = data.ok ? (data.data || []) : [];
44 - } catch (e) {
45 - console.error("Failed to load agent profiles:", e);
46 - this.agentProfiles = [];
47 - }
48 - },
49 -
39 async loadProjects() {
40 try {
41 const response = await fetchApi("/projects", {
@@ -72,7 +61,6 @@ const model = {
61 body: JSON.stringify({
62 action: "list",
63 project_name: this.projectName || null,
75 - agent_profile: this.agentProfileKey || null,
64 }),
65 });
66 const result = await response.json().catch(() => ({}));
@@ -90,6 +78,24 @@ const model = {
78 }
79 },
80
81 + get filteredSkills() {
82 + return this.skills.filter((skill) => matchesSearchQuery(this.skillSearch, [
83 + skill.name,
84 + skill.description,
85 + skill.path,
86 + skill.scope,
87 + skill.project_name,
88 + ]));
89 + },
90 +
91 + get skillSearchActive() {
92 + return !!String(this.skillSearch || "").trim();
93 + },
94 +
95 + clearSkillSearch() {
96 + this.skillSearch = "";
97 + },
98 +
99 async deleteSkill(skill) {
100 if (!skill) return;
101 try {
webui/components/settings/skills/skills-settings.html
+10 -10
@@ -15,18 +15,18 @@
15 <span>List Skills</span>
16 </a>
17 </li>
18 - <li>
19 - <a href="#section-skills-scan">
20 - <span class="material-symbols-outlined" aria-hidden="true">radar</span>
21 - <span>Scan Skills</span>
22 - </a>
23 - </li>
18 <li>
19 <a href="#section-skills-import">
20 <img src="/public/skills_add.svg" alt="Skills" />
21 <span>Import Skills</span>
22 </a>
23 </li>
24 + <li>
25 + <a href="#section-skills-scan">
26 + <span class="material-symbols-outlined" aria-hidden="true">radar</span>
27 + <span>Scan Skills</span>
28 + </a>
29 + </li>
30 </ul>
31 </nav>
32
@@ -34,13 +34,13 @@
34 <x-component path="settings/skills/list.html"></x-component>
35 </div>
36
37 - <div id="section-skills-scan" class="section">
38 - <x-component path="settings/skills/scan.html"></x-component>
39 - </div>
40 -
37 <div id="section-skills-import" class="section">
38 <x-component path="settings/skills/import.html"></x-component>
39 </div>
40 +
41 + <div id="section-skills-scan" class="section">
42 + <x-component path="settings/skills/scan.html"></x-component>
43 + </div>
44 </div>
45 </template>
46 </div>