library: hundreds of skills no longer make the Skills tab crawl — they're grouped by the GitHub repository they came from (owner/repo, how many, an Update N for those a check found changed, which agents have all or some of them, and a link to the repository), those on this computer in a group of their own, each group folded to its heading line and opened with a click, remembered in the browser; with more than 40 skills from several places the groups start folded. A filter over the skills (name, description, repository) shows when there are more than 8, opening the groups that have matches, and a filter or a fold draws only the groups again, not the page. Every row was drawn with a chip for each agent, 4400 icons for 400 skills, and each redraw, scroll and hover laid out and painted all of them: a row out of view is now skipped by the browser (content-visibility) and an agent's icon is made once and copied rather than made afresh (and probed) for every chip. On 400 skills from 6 repositories and 100 local ones, drawing the tab went from 131ms to 17ms folded (55ms with every group open), a pointer sweeping the list from 1.4–1.65s of main-thread work to 0.4s (0.67s all open), and a 6000px scroll from 0.65–0.71s to 0.29s (0.49s)
This commit is contained in:
3 files changed
+164
-6
No files matched your search
@@ -1137,6 +1137,15 @@ const I18N = {
|
||||
"Update {n}": "更新 {n} 个",
|
||||
"Fetch {name} from GitHub again": "从 GitHub 重新获取 {name}",
|
||||
"Fetch the {n} skills GitHub changed again": "从 GitHub 重新获取有改动的 {n} 个技能",
|
||||
"Filter {n} skills…": "筛选 {n} 个技能…",
|
||||
"No skill matches “{q}”.": "没有与“{q}”匹配的技能。",
|
||||
"On this computer": "本机",
|
||||
"1 skill": "1 个技能",
|
||||
"{n} of {total} skills": "{total} 个技能中的 {n} 个",
|
||||
"{agent} has all of them": "{agent} 已有其中全部技能",
|
||||
"{agent} has {n} of them": "{agent} 已有其中 {n} 个",
|
||||
"Open {repo} on GitHub": "在 GitHub 上打开 {repo}",
|
||||
"Show its skills": "展开技能",
|
||||
"1 skill has an update": "1 个技能有更新",
|
||||
"{n} skills have updates": "{n} 个技能有更新",
|
||||
"Every skill is up to date": "所有技能都是最新的",
|
||||
|
||||
@@ -311,3 +311,16 @@ input.lib-setname:focus { border-color: var(--accent); box-shadow: 0 0 0 3px col
|
||||
.lib-project .lib-projskill { border-top: 1px solid var(--line-2); }
|
||||
.lib-projfoot { margin: 0; padding: 8px 12px 10px 49px; border-top: 1px solid var(--line-2); }
|
||||
.lib-projfoot + .lib-projfoot { border-top: 0; padding-top: 0; }
|
||||
|
||||
/* the library's skills: a card for each GitHub repository they came from,
|
||||
and one for those on this computer, folded to its heading line */
|
||||
.lib-groups { display: flex; flex-direction: column; gap: 8px; }
|
||||
.lib-grouphead .sub { font-size: 11px; }
|
||||
.lib-grouphead > .lib-logo, .lib-grouphead > .lib-glyph { align-self: center; margin-top: 0; }
|
||||
.lib-grouphead .lib-updall { height: 24px; padding: 0 9px; font-size: 12px; }
|
||||
.lib-have .ic.some { opacity: .4; }
|
||||
#view-library .row-head .lib-skillq { width: 190px; height: 24px; margin: 0; flex: 0 1 auto; min-width: 90px; }
|
||||
.lib-skillq::-webkit-search-cancel-button { -webkit-appearance: none; }
|
||||
/* a skill row out of view is neither laid out nor painted until it nears it */
|
||||
.lib-skill, .lib-projskill { content-visibility: auto; contain-intrinsic-size: auto 76px; }
|
||||
.lib-grouphead .lib-rowacts { min-width: 26px; }
|
||||
@@ -93,6 +93,16 @@
|
||||
return s;
|
||||
}
|
||||
|
||||
// An agent's icon, made once and copied: a list of hundreds of skills has
|
||||
// a chip for each agent on every row, and making each icon afresh (its
|
||||
// image, and a probe of whether it loads) was most of drawing the list.
|
||||
const icons = new Map();
|
||||
function agentIcon(name) {
|
||||
let i = icons.get(name);
|
||||
if (!i) icons.set(name, (i = icon(name)));
|
||||
return i.cloneNode(true);
|
||||
}
|
||||
|
||||
// Agent chips for a server or a skill: each agent that could have it, lit
|
||||
// when it does. A chip whose agent couldn't be given it says why.
|
||||
function agentChips(all, on, onChange, opts = {}) {
|
||||
@@ -102,7 +112,7 @@
|
||||
const has = on.includes(a.id);
|
||||
const c = el("button", "lib-ag" + (has ? " on" : ""));
|
||||
c.dataset.agent = a.id;
|
||||
c.append(icon(a.icon));
|
||||
c.append(agentIcon(a.icon));
|
||||
if (opts.names) c.append(el("span", "n", a.name));
|
||||
const problem = opts.problems?.[a.id];
|
||||
const blocked = opts.blocked?.(a);
|
||||
@@ -997,8 +1007,10 @@
|
||||
if (lib.skills.length) {
|
||||
const rh = el("div", "row-head");
|
||||
rh.append(el("span", "label", t("In the library")));
|
||||
const box = el("div", "lib-groups");
|
||||
if (lib.skills.length > 8) rh.append(skillFilter(box, all));
|
||||
const fresh = lib.skills.filter((s) => s.kind === "github" || s.origin);
|
||||
if (fresh.length) rh.append(el("span", "grow"));
|
||||
rh.append(el("span", "grow"));
|
||||
if (lib.skills.some((s) => s.kind === "github")) {
|
||||
const c = button(checking ? t("Checking…") : t("Check for updates"), "lib-updall", () => checkSkills());
|
||||
c.title = t("Ask GitHub which skills changed since they were installed");
|
||||
@@ -1029,9 +1041,8 @@
|
||||
rh.append(u);
|
||||
}
|
||||
body.append(rh);
|
||||
const list = el("div", "list lib-list");
|
||||
for (const s of lib.skills) list.append(skillRow(s, all));
|
||||
body.append(list);
|
||||
drawSkills(box, all);
|
||||
body.append(box);
|
||||
}
|
||||
if (lib.foundSkills.length) {
|
||||
const rh = el("div", "row-head");
|
||||
@@ -1047,6 +1058,131 @@
|
||||
body.append(discover("skills"));
|
||||
}
|
||||
|
||||
// ---------- the library's skills, by where they came from ----------
|
||||
|
||||
// Hundreds of skills were one list of rows, every one of them drawn with
|
||||
// a chip for each agent — thousands of icons laid out and painted on each
|
||||
// redraw, scroll and hover. They're grouped by the GitHub repository they
|
||||
// came from (the ones on this computer together): a folded group draws
|
||||
// only its heading, and a filter or a fold draws the groups again, not
|
||||
// the page.
|
||||
let skillQuery = ""; // what the filter over the skills holds
|
||||
const unfiltered = new Set(); // groups folded while filtering, till the filter changes
|
||||
let folds = {}; // group → true when folded, false when opened by hand
|
||||
try { folds = JSON.parse(localStorage.getItem("magpie.libSkillFolds") || "{}") || {}; } catch {}
|
||||
function saveFolds() { try { localStorage.setItem("magpie.libSkillFolds", JSON.stringify(folds)); } catch {} }
|
||||
const repoOf = (u) => (u || "").replace(/^https:\/\/github\.com\//, "").split("/").slice(0, 2).join("/");
|
||||
|
||||
function skillGroups() {
|
||||
const by = new Map();
|
||||
for (const s of lib.skills) {
|
||||
const repo = s.kind === "github" ? repoOf(s.source) : s.origin ? repoOf(s.origin) : "";
|
||||
const key = repo ? "gh:" + repo.toLowerCase() : "local";
|
||||
let g = by.get(key);
|
||||
if (!g) by.set(key, (g = { key, repo, skills: [] }));
|
||||
g.skills.push(s);
|
||||
}
|
||||
const out = [...by.values()].sort((a, b) => (a.key === "local") - (b.key === "local") || a.repo.localeCompare(b.repo));
|
||||
for (const g of out) g.skills.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return out;
|
||||
}
|
||||
|
||||
function skillFilter(box, all) {
|
||||
const f = el("input", "lib-filter lib-skillq");
|
||||
f.type = "search";
|
||||
f.dataset.lib = "skillq";
|
||||
f.placeholder = t("Filter {n} skills…", { n: lib.skills.length });
|
||||
f.spellcheck = false;
|
||||
f.autocomplete = "off";
|
||||
f.value = skillQuery;
|
||||
f.oninput = () => { skillQuery = f.value; unfiltered.clear(); drawSkills(box, all); };
|
||||
f.onkeydown = (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Escape" && f.value) { e.preventDefault(); f.value = ""; f.oninput(); }
|
||||
};
|
||||
return f;
|
||||
}
|
||||
|
||||
function drawSkills(box, all) {
|
||||
const groups = skillGroups();
|
||||
const q = skillQuery.trim().toLowerCase();
|
||||
const hitsOf = (g) => (q ? g.skills.filter((s) => (s.name + " " + (s.description || "") + " " + g.repo).toLowerCase().includes(q)) : g.skills);
|
||||
box.replaceChildren();
|
||||
let shown = 0;
|
||||
if (groups.length === 1) {
|
||||
// from one place only: the list as it always was
|
||||
const hits = hitsOf(groups[0]);
|
||||
if (hits.length) {
|
||||
const list = el("div", "list lib-list");
|
||||
for (const s of hits) list.append(skillRow(s, all));
|
||||
box.append(list);
|
||||
shown = hits.length;
|
||||
}
|
||||
} else {
|
||||
// many skills from several places start folded, each group a line
|
||||
const many = lib.skills.length > 40;
|
||||
for (const g of groups) {
|
||||
const hits = hitsOf(g);
|
||||
if (!hits.length) continue;
|
||||
shown++;
|
||||
box.append(groupCard(g, hits, all, q ? unfiltered.has(g.key) : folds[g.key] ?? many, !!q));
|
||||
}
|
||||
}
|
||||
if (!shown) box.append(el("div", "list lib-none", t("No skill matches “{q}”.", { q: skillQuery.trim() })));
|
||||
}
|
||||
|
||||
function groupCard(g, hits, all, folded, filtering) {
|
||||
const card = el("div", "list lib-card lib-group");
|
||||
card.dataset.group = g.key;
|
||||
const head = el("div", "row lib-row click lib-grouphead" + (folded ? "" : " open"));
|
||||
const who = el("div", "who");
|
||||
who.append(el("div", "name" + (g.repo ? " mono" : ""), g.repo || t("On this computer")));
|
||||
const n = g.skills.length;
|
||||
who.append(el("div", "sub", filtering && hits.length !== n ? t("{n} of {total} skills", { n: hits.length, total: n })
|
||||
: n === 1 ? t("1 skill") : t("{n} skills", { n })));
|
||||
const tags = el("div", "lib-tags");
|
||||
const stale = g.skills.filter((s) => s.check?.status === "update");
|
||||
if (stale.length) {
|
||||
const u = button(t("Update {n}", { n: stale.length }), "action lib-updall", async (e, b) => {
|
||||
b.classList.add("busy");
|
||||
b.textContent = t("Updating…");
|
||||
await updateAllSkills(stale.map((s) => s.name));
|
||||
});
|
||||
u.title = stale.length === 1 ? t("Fetch {name} from GitHub again", { name: stale[0].name }) : t("Fetch the {n} skills GitHub changed again", { n: stale.length });
|
||||
tags.append(u);
|
||||
}
|
||||
// which agents have its skills, all of them or some
|
||||
const have = el("div", "lib-have");
|
||||
for (const a of all) {
|
||||
const k = g.skills.filter((s) => s.agents.includes(a.id)).length;
|
||||
if (!k) continue;
|
||||
const i = agentIcon(a.icon);
|
||||
i.title = k === n ? t("{agent} has all of them", { agent: a.name }) : t("{agent} has {n} of them", { agent: a.name, n: k });
|
||||
if (k < n) i.classList.add("some");
|
||||
have.append(i);
|
||||
}
|
||||
const acts = el("div", "lib-rowacts");
|
||||
if (g.repo) {
|
||||
const o = button("", "lib-icon", () => browse("https://github.com/" + g.repo));
|
||||
o.append(svg(GLYPH.out, 13, 1.4));
|
||||
o.title = t("Open {repo} on GitHub", { repo: g.repo });
|
||||
acts.append(o);
|
||||
}
|
||||
const chev = el("span", "chev");
|
||||
chev.append(svg(CHEV_R, 11, 1.7));
|
||||
const pic = g.repo ? mark(g.skills.find((s) => s.icon)?.icon, GLYPH.skill) : glyph(GLYPH.folder);
|
||||
head.append(pic, who, tags, have, acts, chev);
|
||||
head.title = folded ? t("Show its skills") : t("Hide its skills");
|
||||
head.onclick = () => {
|
||||
if (filtering) { if (folded) unfiltered.delete(g.key); else unfiltered.add(g.key); }
|
||||
else { folds[g.key] = !folded; saveFolds(); }
|
||||
card.replaceWith(groupCard(g, hits, all, !folded, filtering));
|
||||
};
|
||||
card.append(head);
|
||||
if (!folded) for (const s of hits) card.append(skillRow(s, all));
|
||||
return card;
|
||||
}
|
||||
|
||||
// ---------- projects ----------
|
||||
|
||||
// A project is a folder of the user's whose agents get some of the
|
||||
@@ -1468,7 +1604,7 @@
|
||||
who.append(sub);
|
||||
if (f.link) { const src = el("div", "lib-src"); src.append(el("span", "", t("linked from")), pathLink(f.link)); who.append(src); }
|
||||
const have = el("div", "lib-have");
|
||||
for (const id of f.agents) { const a = agentOf(id); if (a) { const i = icon(a.icon); i.title = a.name; have.append(i); } }
|
||||
for (const id of f.agents) { const a = agentOf(id); if (a) { const i = agentIcon(a.icon); i.title = a.name; have.append(i); } }
|
||||
row.append(glyph(GLYPH.skill), who, have);
|
||||
if (f.others?.length) row.append(tag(t("differs in {agents}", { agents: f.others.map(nameOf).join(", ") }), "warn", t("{agents} has another skill by this name; bringing this one in leaves that one as it is", { agents: f.others.map(nameOf).join(", ") })));
|
||||
const b = button(t("Bring in"), "action", () => change("skills/import", { name: f.name }, t("{name} is in the library now", { name: f.name })));
|
||||
|
||||
Reference in new issue
Block a user