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.
This commit is contained in:
Alessandro 2026-06-18 13:14:19 +02:00
parent 7ac9910e1d
commit 352320ef81
7 changed files with 183 additions and 70 deletions

View file

@ -34,8 +34,8 @@ const TAB_ITEMS = Object.freeze([
icon: "school",
sections: [
{ id: "section-skills-list", label: "List Skills", icon: "view_list" },
{ id: "section-skills-scan", label: "Scan Skills", icon: "radar" },
{ id: "section-skills-import", label: "Import Skills", icon: "upload_file" },
{ id: "section-skills-scan", label: "Scan Skills", icon: "radar" },
],
},
{

View file

@ -38,17 +38,6 @@
</select>
</label>
<label class="policy-label">
<span class="policy-label-text">Limit to agent profile:</span>
<select x-model="$store.skillsImportStore.agentProfileKey" class="policy-dropdown"
@change="$store.skillsImportStore.previewImport()">
<option value="">All</option>
<template x-for="agentProfile in $store.skillsImportStore.agentProfiles" :key="agentProfile.key">
<option :value="agentProfile.key" x-text="agentProfile.label"></option>
</template>
</select>
</label>
<label class="policy-label">
<span class="policy-label-text">Namespace:</span>
<input class="text-input" type="text" placeholder="e.g. my-pack"

View file

@ -28,15 +28,16 @@
</select>
</label>
<label class="skills-toolbar-item">
<span class="skills-toolbar-label">Agent profile</span>
<select x-model="$store.skillsListStore.agentProfileKey"
@change="$store.skillsListStore.loadSkills()">
<option value="">All</option>
<template x-for="agentProfile in $store.skillsListStore.agentProfiles" :key="agentProfile.key">
<option :value="agentProfile.key" x-text="agentProfile.label"></option>
</template>
</select>
<label class="skills-search">
<span class="material-symbols-outlined" aria-hidden="true">search</span>
<input type="search" x-model.debounce.150ms="$store.skillsListStore.skillSearch"
placeholder="Search skills" />
<button type="button" class="skills-search-clear"
x-show="$store.skillsListStore.skillSearchActive"
@click="$store.skillsListStore.clearSkillSearch()"
title="Clear search">
<span class="material-symbols-outlined" aria-hidden="true">close</span>
</button>
</label>
<div class="skills-toolbar-actions">
@ -62,8 +63,13 @@
<div class="skills-empty">No skills found.</div>
</template>
<template
x-if="!$store.skillsListStore.loading && !$store.skillsListStore.error && $store.skillsListStore.skills.length > 0 && $store.skillsListStore.filteredSkills.length === 0">
<div class="skills-empty">No skills match this search.</div>
</template>
<div class="skills-list">
<template x-for="skill in $store.skillsListStore.skills" :key="skill.path">
<template x-for="skill in $store.skillsListStore.filteredSkills" :key="skill.path">
<div class="skill-card">
<div class="skill-header">
<div class="skill-heading">
@ -130,6 +136,50 @@
flex: 0 0 auto;
}
.skills-search {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 2.1rem;
flex: 1 1 18rem;
min-width: min(18rem, 100%);
padding: 0.25rem 0.45rem;
border: 1px solid var(--color-border);
border-radius: 7px;
background: var(--color-input);
color: var(--color-text-muted);
}
.skills-search input {
width: 100%;
min-width: 8rem;
border: 0;
background: transparent;
color: var(--color-text);
outline: none;
padding: 0.2rem;
}
.skills-search input::-webkit-search-cancel-button,
.skills-search input::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
display: none;
}
.skills-search-clear {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.45rem;
height: 1.45rem;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
}
@media (max-width: 640px) {
.skills-toolbar-row {
gap: 0.5rem;
@ -140,6 +190,11 @@
min-width: 0;
}
.skills-search {
flex-basis: 100%;
min-width: 0;
}
.skills-toolbar-actions {
width: 100%;
margin-left: 0;

View file

@ -19,9 +19,7 @@ const model = {
namespace: "",
conflict: "skip", // skip|overwrite|rename
projectKey: "", // selected project key, empty means All
agentProfileKey: "", // selected agent profile key, empty means All
projects: [], // available projects options [{key,label}]
agentProfiles: [], // available agent profile options [{key,label}]
preview: null,
result: null,
@ -29,7 +27,6 @@ const model = {
init() {
this.resetState();
this.loadProjects();
this.loadAgentProfiles();
},
resetState() {
@ -46,7 +43,6 @@ const model = {
this.namespace = "";
this.conflict = "skip";
this.projectKey = "";
this.agentProfileKey = "";
},
async loadProjects() {
@ -59,16 +55,6 @@ const model = {
}
},
async loadAgentProfiles() {
try {
const data = await api.callJsonApi("/agents", { action: "list" });
this.agentProfiles = data.ok ? (data.data || []) : [];
} catch (e) {
console.error("Failed to load agent profiles:", e);
this.agentProfiles = [];
}
},
async handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
@ -100,9 +86,6 @@ const model = {
formData.append("project_name", this.projectKey);
}
if (this.agentProfileKey) {
formData.append("agent_profile", this.agentProfileKey);
}
return formData;
},

View file

@ -3,18 +3,23 @@ import { store as fileBrowserStore } from "/components/modals/file-browser/file-
const fetchApi = globalThis.fetchApi;
function matchesSearchQuery(query, values) {
const normalized = String(query || "").trim().toLowerCase();
if (!normalized) return true;
return values.some((value) => String(value ?? "").toLowerCase().includes(normalized));
}
const model = {
loading: false,
error: "",
skills: [],
projects: [],
projectName: "",
agentProfiles: [],
agentProfileKey: "",
skillSearch: "",
async init() {
this.resetState();
await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
await this.loadProjects();
await this.loadSkills();
},
@ -24,29 +29,13 @@ const model = {
this.skills = [];
this.projects = [];
this.projectName = "";
this.agentProfiles = [];
this.agentProfileKey = "";
this.skillSearch = "";
},
onClose() {
this.resetState();
},
async loadAgentProfiles() {
try {
const response = await fetchApi("/agents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "list" }),
});
const data = await response.json().catch(() => ({}));
this.agentProfiles = data.ok ? (data.data || []) : [];
} catch (e) {
console.error("Failed to load agent profiles:", e);
this.agentProfiles = [];
}
},
async loadProjects() {
try {
const response = await fetchApi("/projects", {
@ -72,7 +61,6 @@ const model = {
body: JSON.stringify({
action: "list",
project_name: this.projectName || null,
agent_profile: this.agentProfileKey || null,
}),
});
const result = await response.json().catch(() => ({}));
@ -90,6 +78,24 @@ const model = {
}
},
get filteredSkills() {
return this.skills.filter((skill) => matchesSearchQuery(this.skillSearch, [
skill.name,
skill.description,
skill.path,
skill.scope,
skill.project_name,
]));
},
get skillSearchActive() {
return !!String(this.skillSearch || "").trim();
},
clearSkillSearch() {
this.skillSearch = "";
},
async deleteSkill(skill) {
if (!skill) return;
try {

View file

@ -15,18 +15,18 @@
<span>List Skills</span>
</a>
</li>
<li>
<a href="#section-skills-scan">
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
<span>Scan Skills</span>
</a>
</li>
<li>
<a href="#section-skills-import">
<img src="/public/skills_add.svg" alt="Skills" />
<span>Import Skills</span>
</a>
</li>
<li>
<a href="#section-skills-scan">
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
<span>Scan Skills</span>
</a>
</li>
</ul>
</nav>
@ -34,13 +34,13 @@
<x-component path="settings/skills/list.html"></x-component>
</div>
<div id="section-skills-scan" class="section">
<x-component path="settings/skills/scan.html"></x-component>
</div>
<div id="section-skills-import" class="section">
<x-component path="settings/skills/import.html"></x-component>
</div>
<div id="section-skills-scan" class="section">
<x-component path="settings/skills/scan.html"></x-component>
</div>
</div>
</template>
</div>