mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
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:
parent
7ac9910e1d
commit
352320ef81
7 changed files with 183 additions and 70 deletions
|
|
@ -126,6 +126,12 @@ def test_settings_skills_scan_section_and_prompt_assets_are_present():
|
|||
assert "section-skills-scan" in settings_store
|
||||
assert "settings/skills/scan.html" in skills_settings
|
||||
assert "settings/skills/import.html" in skills_settings
|
||||
assert settings_store.index("section-skills-import") < settings_store.index(
|
||||
"section-skills-scan"
|
||||
)
|
||||
assert skills_settings.index("section-skills-import") < skills_settings.index(
|
||||
"section-skills-scan"
|
||||
)
|
||||
assert "scanSelectedFile()" in import_template
|
||||
assert "snyk-agent-scan@latest --json --no-bootstrap" in scan_prompt
|
||||
assert "E004" in scan_checks_text
|
||||
|
|
@ -133,3 +139,77 @@ def test_settings_skills_scan_section_and_prompt_assets_are_present():
|
|||
assert "W008" in scan_checks_text
|
||||
assert "W011" in scan_checks_text
|
||||
assert "W012" in scan_checks_text
|
||||
|
||||
|
||||
def test_list_and_import_skills_filters_are_project_only():
|
||||
list_template = (
|
||||
PROJECT_ROOT
|
||||
/ "webui"
|
||||
/ "components"
|
||||
/ "settings"
|
||||
/ "skills"
|
||||
/ "list.html"
|
||||
).read_text(encoding="utf-8")
|
||||
list_store = (
|
||||
PROJECT_ROOT
|
||||
/ "webui"
|
||||
/ "components"
|
||||
/ "settings"
|
||||
/ "skills"
|
||||
/ "skills-list-store.js"
|
||||
).read_text(encoding="utf-8")
|
||||
import_template = (
|
||||
PROJECT_ROOT
|
||||
/ "webui"
|
||||
/ "components"
|
||||
/ "settings"
|
||||
/ "skills"
|
||||
/ "import.html"
|
||||
).read_text(encoding="utf-8")
|
||||
import_store = (
|
||||
PROJECT_ROOT
|
||||
/ "webui"
|
||||
/ "components"
|
||||
/ "settings"
|
||||
/ "skills"
|
||||
/ "skills-import-store.js"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert "Agent profile" not in list_template
|
||||
assert "agentProfile" not in list_store
|
||||
assert "agent_profile" not in list_store
|
||||
assert "Project" in list_template
|
||||
assert "project_name" in list_store
|
||||
assert "Limit to agent profile" not in import_template
|
||||
assert "agentProfile" not in import_store
|
||||
assert "agent_profile" not in import_store
|
||||
assert "Limit to project" in import_template
|
||||
assert "project_name" in import_store
|
||||
|
||||
|
||||
def test_list_skills_has_mcp_style_search():
|
||||
list_template = (
|
||||
PROJECT_ROOT
|
||||
/ "webui"
|
||||
/ "components"
|
||||
/ "settings"
|
||||
/ "skills"
|
||||
/ "list.html"
|
||||
).read_text(encoding="utf-8")
|
||||
list_store = (
|
||||
PROJECT_ROOT
|
||||
/ "webui"
|
||||
/ "components"
|
||||
/ "settings"
|
||||
/ "skills"
|
||||
/ "skills-list-store.js"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert 'type="search"' in list_template
|
||||
assert "placeholder=\"Search skills\"" in list_template
|
||||
assert "skills-search-clear" in list_template
|
||||
assert "filteredSkills" in list_template
|
||||
assert "No skills match this search." in list_template
|
||||
assert "skillSearch" in list_store
|
||||
assert "matchesSearchQuery" in list_store
|
||||
assert "clearSkillSearch()" in list_store
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue