diff --git a/static/js/skills.js b/static/js/skills.js index 4a0adbe11..7f70072f7 100644 --- a/static/js/skills.js +++ b/static/js/skills.js @@ -91,7 +91,18 @@ export async function loadSkills(cascade = false) { try { const res = await fetch(`${API}/api/skills`); const data = await res.json(); - skills = data.skills || []; + // Dedupe by name (case-insensitive) — the API has occasionally + // returned the same skill twice (built-in shadow + user copy, or + // a write-then-read race), and rendering both made the duplicate + // detector mark BOTH entries as the "recommended" keeper. + const _seen = new Set(); + skills = (data.skills || []).filter(sk => { + const k = String(sk?.name || sk?.id || '').toLowerCase(); + if (!k) return true; + if (_seen.has(k)) return false; + _seen.add(k); + return true; + }); _loadSkillApprovalThreshold(); // Built-in capabilities are no longer surfaced in the Skills menu. loaded = true; @@ -392,21 +403,11 @@ function _openSkillMenu(btn, card, sk, name, isPublished) { }; if (isPublished) mk(_ICON.unpublish, 'Unpublish', {}, () => _setSkillStatus(name, 'draft')); else mk(_ICON.approve, 'Publish', {}, () => _setSkillStatus(name, 'published')); - mk(_ICON.edit, 'Edit', {}, async () => { - if (!card.classList.contains('doclib-card-expanded')) await _expandSkillCard(card, name); - _toggleSkillEdit(card, name); - }); - mk(_ICON.test, 'Test', {}, () => _testSkill(card, name)); - // Audit kicks off the bulk audit-all loop (test → judge → fix → retry → demote). - // Starts at the top of the list and walks down. - mk(_ICON.test, 'Audit', {}, () => _auditAllSkills()); - mk(_ICON.del, 'Delete', { danger: true }, () => _deleteSkill(name, card)); - - // Select — enters bulk-select mode and pre-selects this skill. Same pattern - // as the email/documents/brain Select item, with the email bullet icon. + // Select — moved up to 2nd so it sits next to Publish/Unpublish + // (bulk actions cluster at the top of the menu). const selItem = document.createElement('button'); selItem.className = 'skill-kebab-item'; - selItem.innerHTML = 'Select'; + selItem.innerHTML = 'Select'; selItem.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); @@ -416,6 +417,15 @@ function _openSkillMenu(btn, card, sk, name, isPublished) { }); menu.appendChild(selItem); + mk(_ICON.edit, 'Edit', {}, async () => { + if (!card.classList.contains('doclib-card-expanded')) await _expandSkillCard(card, name); + _toggleSkillEdit(card, name); + }); + mk(_ICON.test, 'Test', {}, () => _testSkill(card, name)); + // Audit kicks off the bulk audit-all loop (test → judge → fix → retry → demote). + mk(_ICON.test, 'Audit', {}, () => _auditAllSkills()); + mk(_ICON.del, 'Delete', { danger: true }, () => _deleteSkill(name, card)); + // Mobile-only Cancel — mirrors the email/documents/brain popup pattern. // CSS hides `.dropdown-cancel-mobile` on desktop where outside-click // already dismisses cleanly.