From 8053d6a50aaf014a604e0661364fb544f7586b51 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Sat, 13 Jun 2026 22:49:48 +0900 Subject: [PATCH] Skills: dedupe by name + move Select to 2nd in kebab menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The API occasionally returns the same skill twice (built-in shadow vs user copy, or a write/read race) which made the duplicate-detector tag BOTH copies as the "recommended" keeper (the find-skills card showing duplicate #1 twice). Loading now filters out repeats by lowercased name before render. - Reordered the per-skill kebab menu: Publish/Unpublish → Select → Edit → Test → Audit → Delete. Select previously sat at the bottom; lifting it next to Publish puts the bulk-mode entry point with the other bulk-style action. --- static/js/skills.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) 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.