library: skills are picked to turn on or off together, and a part of a list (lark-*) has its own chips (#791, mintonight: 资源库中local中的技能,能不能支持多选分组,或者支持多选来开启或关闭 … lark-* 我希望把他们归到一起统一来管理) — the skills on this computer are one list with no heading when they're all from there, so #787's group chips never showed for them, and Feishu's lark-* skills were turned on or off one row at a time. A part of a big list (its skills' shared name start, or their folder) now has chips on its heading, as a repository's heading does: lit when an agent has them all, half lit when it has some, a click gives it every one or takes them all, All does it for every agent; a click doesn't fold the part. Select, beside Turn all on, puts a box on every skill's row and on every heading (ticked when all under it are picked, half with some), a row's click picks it instead of opening its SKILL.md, and a bar that stays in view under the list says how many are picked, has a box for every skill the filter shows ("All 4 that match"), the same chips for the picked ones, Clear and Done; the picks stay through a change and Done takes the boxes away. A pick doesn't scroll the page. zh and ja strings added. pick-skills.test.cjs (WebKit, en and zh): the lark part's Codex chip posts the four lark skills, rows and headings pick, the bar's chips post the picked ones, the filter's All picks what it shows, Done ends it; it fails on the old code. Looked at in light and dark. group-skill-chips, every-skill, library-all, library-items, library-toggle-status, library-row-open-scroll, own-skill, remove-all-skills, shared-skills, import-all-skills, library-own-folder and library-loading pass on WebKit; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass.
This commit is contained in:
1 parent
e8cd4bac8c
commit
26706c3371
4 files changed
+292
-11
No files matched your search
@@ -2790,6 +2790,18 @@ const I18N = {
|
||||
"Give {agent} all of them": "为 {agent} 启用其中全部技能",
|
||||
"Every agent has all of them — click to take them from every one": "每个 Agent 都已有其中全部技能,点击全部关闭",
|
||||
"Give all of them to every agent": "为每个 Agent 启用其中全部技能",
|
||||
"{name} is on for {agents}": "已为 {agents} 启用 {name}",
|
||||
"{name} is off for {agents}": "已从 {agents} 中关闭 {name}",
|
||||
"{n} skills are on for {agents}": "已为 {agents} 启用 {n} 个技能",
|
||||
"{n} skills are off for {agents}": "已从 {agents} 中关闭 {n} 个技能",
|
||||
"Select {name}": "选中 {name}",
|
||||
"Clear": "清空",
|
||||
"Stop picking skills": "结束多选",
|
||||
"Pick some skills and turn them on or off together": "选中多个技能,一起开启或关闭",
|
||||
"All {n} that match": "全部 {n} 个匹配项",
|
||||
"All {n}": "全部 {n} 个",
|
||||
"Pick skills to turn them on or off together": "勾选技能,再用右侧图标一起开启或关闭",
|
||||
"Unpick them all": "取消全部选中",
|
||||
"{repo}'s skills are on for {agents}": "已为 {agents} 启用 {repo} 的全部技能",
|
||||
"{repo}'s skills are off for {agents}": "已从 {agents} 中关闭 {repo} 的全部技能",
|
||||
"Open {repo} on GitHub": "在 GitHub 上打开 {repo}",
|
||||
@@ -5724,6 +5736,18 @@ const I18N = {
|
||||
"Give {agent} all of them": "{agent} ですべてオンにする",
|
||||
"Every agent has all of them — click to take them from every one": "すべてのエージェントがすべて持っています。クリックですべてオフ",
|
||||
"Give all of them to every agent": "すべてのエージェントですべてオンにする",
|
||||
"{name} is on for {agents}": "{agents} で {name} をオンにしました",
|
||||
"{name} is off for {agents}": "{agents} で {name} をオフにしました",
|
||||
"{n} skills are on for {agents}": "{agents} で {n} 個のスキルをオンにしました",
|
||||
"{n} skills are off for {agents}": "{agents} で {n} 個のスキルをオフにしました",
|
||||
"Select {name}": "{name} を選択",
|
||||
"Clear": "クリア",
|
||||
"Stop picking skills": "選択を終了",
|
||||
"Pick some skills and turn them on or off together": "複数のスキルを選んでまとめてオン・オフする",
|
||||
"All {n} that match": "一致する {n} 個すべて",
|
||||
"All {n}": "{n} 個すべて",
|
||||
"Pick skills to turn them on or off together": "スキルを選び、右のアイコンでまとめてオン・オフ",
|
||||
"Unpick them all": "選択をすべて解除",
|
||||
"{repo}'s skills are on for {agents}": "{agents} で {repo} のスキルをすべてオンにしました",
|
||||
"{repo}'s skills are off for {agents}": "{agents} で {repo} のスキルをすべてオフにしました",
|
||||
"Open {repo} on GitHub": "GitHub で {repo} を開く",
|
||||
|
||||
@@ -477,3 +477,13 @@ input.lib-setname:focus { border-color: var(--accent); box-shadow: 0 0 0 3px col
|
||||
.lib-row.lib-subhead:hover { background: color-mix(in srgb, var(--card-2) 70%, var(--pill)); }
|
||||
/* no color-mix() before Safari 16.2 (#220): the sticky head stays opaque */
|
||||
@supports not (color: color-mix(in srgb, red 50%, blue)) { .lib-head { background: var(--bg); } }
|
||||
/* skills picked to turn on or off together (#791): a box on each row and
|
||||
heading, and a bar that stays in view under the list while it's long */
|
||||
.lib-pickbox { margin: 0; flex: none; }
|
||||
.lib-vl > .row.lib-skill.picked { background: var(--accent-soft); }
|
||||
.lib-pickbar { position: sticky; bottom: 12px; z-index: 3; margin-top: 8px; min-height: 44px; border: 1px solid var(--line); border-radius: 12px; background: var(--card); box-shadow: var(--shadow); }
|
||||
.lib-pickall { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--fg-2); white-space: nowrap; }
|
||||
.lib-pickn { font-size: 12px; font-weight: 500; color: var(--fg); white-space: nowrap; }
|
||||
.lib-pickn.none { font-weight: 400; color: var(--faint); white-space: normal; }
|
||||
.lib-pickbar .lib-updall { height: 24px; padding: 0 9px; font-size: 12px; flex: none; }
|
||||
.lib-subhead > .lib-agents { margin-left: auto; }
|
||||
+106
-11
@@ -1693,6 +1693,8 @@
|
||||
body.append(intro(t("A skill is a folder with a SKILL.md an agent loads when it's needed. The library keeps each one once and links it into the agents you pick.")));
|
||||
body.append(installCard());
|
||||
const all = skillAgents();
|
||||
for (const n of picked) if (!skillNamed(n)) picked.delete(n);
|
||||
if (lib.skills.length < 2) picking = false;
|
||||
if (lib.skills.length) {
|
||||
const rh = el("div", "row-head lib-skillshead");
|
||||
rh.append(el("span", "label", t("In the library")));
|
||||
@@ -1701,7 +1703,7 @@
|
||||
const fresh = lib.skills.filter((s) => s.kind === "github" || s.origin);
|
||||
rh.append(el("span", "grow"));
|
||||
if (lib.skills.length > 1) rh.append(sortBy("libSkills", SKILL_SORTS(), () => { if (box.isConnected) drawSkills(box, all); }));
|
||||
if (lib.skills.length > 1 && all.length) rh.append(...everySkillButtons(all));
|
||||
if (lib.skills.length > 1 && all.length) rh.append(pickButton(), ...everySkillButtons(all));
|
||||
if (byAgentRows("skills").length) rh.append(byAgentButton("skills"));
|
||||
if (lib.skills.length > 1) rh.append(removeEverySkillButton());
|
||||
if (lib.skills.some((s) => s.kind === "github")) {
|
||||
@@ -1736,6 +1738,7 @@
|
||||
body.append(rh);
|
||||
drawSkills(box, all);
|
||||
body.append(box);
|
||||
if (picking) body.append(pickBar(all));
|
||||
}
|
||||
if (lib.foundSkills.length) {
|
||||
const rh = el("div", "row-head");
|
||||
@@ -1779,6 +1782,8 @@
|
||||
// drawn, the rest is room kept for them, so a scroll through a thousand
|
||||
// skills draws a few rows now and then instead of laying out and painting
|
||||
// each as it comes into view.
|
||||
let picking = false; // the skills' rows have a box each, to pick some (#791)
|
||||
const picked = new Set(); // the names picked
|
||||
let skillQuery = ""; // what the filter over the skills holds
|
||||
let skillTimer = 0; // the filter's redraw, waiting for typing to pause
|
||||
const unfiltered = new Set(); // groups folded while filtering, till the filter changes
|
||||
@@ -1921,7 +1926,7 @@
|
||||
}
|
||||
if (!cards.length) cards.push(el("div", "list lib-none", t("No skill matches “{q}”.", { q: skillQuery.trim() })));
|
||||
box.replaceChildren(...cards);
|
||||
if (box.isConnected) syncLists();
|
||||
if (box.isConnected) { syncLists(); if (picking) syncPicks(); } // the bar's All counts what the filter shows
|
||||
}
|
||||
|
||||
// A group's list: its skills, or its parts, each a heading over its own.
|
||||
@@ -1935,17 +1940,18 @@
|
||||
if (!ph.length) continue;
|
||||
const fk = g.key + "\n" + p.key;
|
||||
const folded = filtering ? unfiltered.has(fk) : folds[fk] ?? g.skills.length > MANY;
|
||||
items.push({ key: ["p", p.key, folded, filtering, ph.length].join("\n"), h: SUB_H, make: () => partHead(g, p, ph, folded, filtering, fk, redraw) });
|
||||
items.push({ key: ["p", p.key, folded, filtering, ph.length].join("\n"), h: SUB_H, make: () => partHead(g, p, ph, folded, filtering, fk, redraw, all) });
|
||||
if (!folded) for (const s of ph) items.push(row(s));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function partHead(g, p, hits, folded, filtering, fk, redraw) {
|
||||
function partHead(g, p, hits, folded, filtering, fk, redraw, all) {
|
||||
const head = el("div", "row lib-row click lib-subhead" + (folded ? "" : " open"));
|
||||
const chev = el("span", "chev");
|
||||
chev.append(svg(CHEV_R, 10, 1.7));
|
||||
const n = p.skills.length;
|
||||
if (picking) head.append(pickAll(hits));
|
||||
head.append(chev, el("span", "name" + (p.mono && p.key ? " mono" : ""), p.label),
|
||||
el("span", "sub", filtering && hits.length !== n ? t("{n} of {total}", { n: hits.length, total: n }) : String(n)));
|
||||
// a dot when GitHub changed some of its skills
|
||||
@@ -1955,6 +1961,11 @@
|
||||
dot.title = stale === 1 ? t("1 skill has an update") : t("{n} skills have updates", { n: stale });
|
||||
head.append(dot);
|
||||
}
|
||||
// its skills on or off for an agent at once, as a group's are (#791)
|
||||
if (all.length) {
|
||||
const live = p.skills.map((s) => skillNamed(s.name) || s);
|
||||
head.append(el("span", "grow"), groupChips(live, all, null));
|
||||
}
|
||||
head.title = folded ? t("Show its skills") : t("Hide its skills");
|
||||
head.onclick = () => {
|
||||
if (filtering) { if (folded) unfiltered.delete(fk); else unfiltered.add(fk); }
|
||||
@@ -1992,7 +2003,7 @@
|
||||
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);
|
||||
}
|
||||
const have = groupChips(g, all);
|
||||
const have = groupChips(g.skills, all, g.repo || t("On this computer"));
|
||||
const acts = el("div", "lib-rowacts");
|
||||
if (g.repo) {
|
||||
const o = button("", "lib-icon", () => browse("https://github.com/" + g.repo));
|
||||
@@ -2003,6 +2014,7 @@
|
||||
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);
|
||||
if (picking) head.append(pickAll(hits));
|
||||
head.append(pic, who, tags, have, acts, chev);
|
||||
head.title = folded ? t("Show its skills") : t("Hide its skills");
|
||||
head.onclick = () => {
|
||||
@@ -2020,9 +2032,12 @@
|
||||
// mintonight: a repository's skills were on or off one row at a time).
|
||||
// A chip is lit when its agent has them all; one with some is half lit,
|
||||
// and a click gives it the rest. All gives them to every agent shown.
|
||||
function groupChips(g, all) {
|
||||
const n = g.skills.length, names = g.skills.map((s) => s.name);
|
||||
const count = (id) => g.skills.filter((s) => s.agents?.includes(id)).length;
|
||||
// A part's chips (#791, mintonight: the lark-* skills of Feishu's pack
|
||||
// on or off together) and a pick's are the same, said of so many skills
|
||||
// rather than of a repository.
|
||||
function groupChips(skills, all, repo) {
|
||||
const n = skills.length, names = skills.map((s) => s.name);
|
||||
const count = (id) => skills.filter((s) => s.agents?.includes(id)).length;
|
||||
const full = all.filter((a) => count(a.id) === n).map((a) => a.id);
|
||||
const box = agentChips(all, full, async (next) => {
|
||||
const on = next.filter((id) => !full.includes(id)), off = full.filter((id) => !next.includes(id));
|
||||
@@ -2034,9 +2049,10 @@
|
||||
if (!v) return;
|
||||
take(v);
|
||||
const who = (ids) => ids.map(nameOf).join(", ");
|
||||
report(v.result, on.length
|
||||
? t("{repo}'s skills are on for {agents}", { repo: g.repo || t("On this computer"), agents: who(on) })
|
||||
: t("{repo}'s skills are off for {agents}", { repo: g.repo || t("On this computer"), agents: who(off) }));
|
||||
report(v.result, repo != null
|
||||
? (on.length ? t("{repo}'s skills are on for {agents}", { repo, agents: who(on) }) : t("{repo}'s skills are off for {agents}", { repo, agents: who(off) }))
|
||||
: n === 1 ? (on.length ? t("{name} is on for {agents}", { name: names[0], agents: who(on) }) : t("{name} is off for {agents}", { name: names[0], agents: who(off) }))
|
||||
: on.length ? t("{n} skills are on for {agents}", { n, agents: who(on) }) : t("{n} skills are off for {agents}", { n, agents: who(off) }));
|
||||
} catch (e) {
|
||||
status(e.message, "err", 6000);
|
||||
}
|
||||
@@ -2055,6 +2071,77 @@
|
||||
return box;
|
||||
}
|
||||
|
||||
// ---------- some skills, picked, on or off at once ----------
|
||||
|
||||
// Select puts a box on each skill's row, and on each heading for the
|
||||
// skills under it, and a bar under the list turns the ones picked on or
|
||||
// off for an agent at once (#791, mintonight: 能不能支持多选来开启或关闭):
|
||||
// its chips work as a group's do. Done takes the boxes away.
|
||||
function pickButton() {
|
||||
const b = button(picking ? t("Done") : t("Select"), "action lib-updall lib-select", () => {
|
||||
picking = !picking;
|
||||
picked.clear();
|
||||
render();
|
||||
});
|
||||
b.title = picking ? t("Stop picking skills") : t("Pick some skills and turn them on or off together");
|
||||
return b;
|
||||
}
|
||||
|
||||
// the skills the filter shows, in every group, folded or not
|
||||
function shownSkills() {
|
||||
const q = skillQuery.trim().toLowerCase();
|
||||
return skillGroups().flatMap((g) => (q ? g.skills.filter((s) => g.text.get(s).includes(q)) : g.skills));
|
||||
}
|
||||
|
||||
// A box for some skills: ticked when they're all picked, half when some
|
||||
// are; a click picks them all, or none when they all were.
|
||||
function pickBox(names) {
|
||||
const box = el("input", "lib-pickbox");
|
||||
box.type = "checkbox";
|
||||
box._names = names;
|
||||
paintPick(box);
|
||||
box.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const every = names.length && names.every((n) => picked.has(n));
|
||||
for (const n of names) if (every) picked.delete(n); else picked.add(n);
|
||||
syncPicks();
|
||||
};
|
||||
return box;
|
||||
}
|
||||
const pickAll = (skills) => pickBox(skills.map((s) => s.name));
|
||||
function paintPick(box) {
|
||||
const k = box._names.filter((n) => picked.has(n)).length;
|
||||
box.checked = k > 0 && k === box._names.length;
|
||||
box.indeterminate = k > 0 && k < box._names.length;
|
||||
const row = box.parentElement;
|
||||
if (row?.classList.contains("lib-skill")) row.classList.toggle("picked", box.checked);
|
||||
}
|
||||
// every box drawn, and the bar, as the picks are now
|
||||
function syncPicks() {
|
||||
for (const b of page.querySelectorAll(".lib-pickbox")) paintPick(b);
|
||||
const bar = page.querySelector(".lib-pickbar");
|
||||
if (bar) bar.replaceWith(pickBar(skillAgents()));
|
||||
}
|
||||
|
||||
function pickBar(all) {
|
||||
const bar = el("div", "row lib-row lib-pickbar");
|
||||
const shown = shownSkills(), q = skillQuery.trim();
|
||||
const every = el("label", "lib-pickall");
|
||||
every.append(pickAll(shown), el("span", "", q ? t("All {n} that match", { n: shown.length }) : t("All {n}", { n: shown.length })));
|
||||
every.onclick = (e) => e.stopPropagation();
|
||||
const skills = lib.skills.filter((s) => picked.has(s.name));
|
||||
const n = skills.length;
|
||||
bar.append(every, el("span", "lib-pickn" + (n ? "" : " none"), n ? t("{n} selected", { n }) : t("Pick skills to turn them on or off together")), el("span", "grow"));
|
||||
if (n) {
|
||||
bar.append(groupChips(skills, all, null));
|
||||
const c = button(t("Clear"), "lib-updall lib-pickclear", () => { picked.clear(); syncPicks(); });
|
||||
c.title = t("Unpick them all");
|
||||
bar.append(c);
|
||||
}
|
||||
bar.append(button(t("Done"), "action lib-updall lib-pickdone", () => { picking = false; picked.clear(); render(); }));
|
||||
return bar;
|
||||
}
|
||||
|
||||
// ---------- a long list, drawn near the view first ----------
|
||||
|
||||
// A list of items of known heights ({ key, h, make }) drawn in two goes:
|
||||
@@ -2876,6 +2963,14 @@
|
||||
rm.title = t("Remove");
|
||||
acts.append(rm);
|
||||
row.append(mark(s.icon, GLYPH.skill), who, acts, agentChips(all, s.agents, chipsChange("skills/agents", s.name, "skills", (x) => skillRow(x, all)), { problems: s.problems, via: viaFor(s), all: true, always: alwaysFor(s) }));
|
||||
if (picking) {
|
||||
const box = pickBox([s.name]);
|
||||
row.prepend(box);
|
||||
row.classList.toggle("picked", picked.has(s.name));
|
||||
row.onclick = () => box.click();
|
||||
row.title = t("Select {name}", { name: s.name });
|
||||
return row;
|
||||
}
|
||||
row.onclick = () => viewSkill(s);
|
||||
row.title = t("Read {name}'s SKILL.md", { name: s.name });
|
||||
return row;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// Many of the library's skills on or off at once (#791, mintonight: 能不能
|
||||
// 支持多选分组,或者支持多选来开启或关闭 … lark-* 我希望把他们归到一起统一来
|
||||
// 管理). Skills whose names start alike are a part of their list with a
|
||||
// heading, and that heading's chips give an agent all of them or take them
|
||||
// away; Select puts a box on every row and heading, and a bar's chips turn
|
||||
// the ones picked on or off; the bar's box picks every one the filter
|
||||
// shows. English and Chinese; the API is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
const HOME = "/Users/me";
|
||||
const agent = (id, name, icon) => ({ id, name, icon, skills: `${HOME}/.${id}/skills`, mcp: `${HOME}/.${id}/mcp.json` });
|
||||
const local = (name, agents) => ({ name, kind: "folder", source: `${HOME}/.agents/skills/${name}`, description: "d", agents });
|
||||
|
||||
function server(lang, state, posts) {
|
||||
return async (route) => {
|
||||
const req = route.request();
|
||||
const url = new URL(req.url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
|
||||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: { lang, theme: "light" } });
|
||||
if (url.pathname === "/api/library") return json(state.lib);
|
||||
if (url.pathname === "/api/library/skills/agents-some") {
|
||||
const b = req.postDataJSON();
|
||||
posts.push(b);
|
||||
for (const s of state.lib.skills) {
|
||||
if (!b.names.includes(s.name)) continue;
|
||||
const kept = s.agents.filter((a) => !b.agents.includes(a));
|
||||
s.agents = (b.on ? [...kept, ...b.agents] : kept).sort();
|
||||
}
|
||||
return json({ ...state.lib, result: { changed: b.agents } });
|
||||
}
|
||||
if (url.pathname === "/api/providers") return json({ providers: [], presets: [], excluded: [], gateway: { running: true, window: true }, plugins: [] });
|
||||
if (url.pathname === "/api/groups") return json({ groups: [], pools: [] });
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
if (url.host !== "magpie.test") return route.fulfill({ status: 404, body: "" });
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
try { await route.fulfill({ body: await fs.readFile(file), contentType }); } catch { await route.fulfill({ status: 404, body: "" }); }
|
||||
};
|
||||
}
|
||||
|
||||
const L = {
|
||||
en: { part: "4 skills are on for Codex", one: "1 selected", two: "2 selected", six: "6 selected", off: "2 skills are off for Claude Code", match: "All 4 that match", select: "Select", done: "Done" },
|
||||
zh: { part: "已为 Codex 启用 4 个技能", one: "已选 1 个", two: "已选 2 个", six: "已选 6 个", off: "已从 Claude Code 中关闭 2 个技能", match: "全部 4 个匹配项", select: "选择", done: "完成" },
|
||||
};
|
||||
const LARK = ["lark-apps", "lark-base", "lark-doc", "lark-drive"];
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
test(engine + ": many skills on or off at once", async (t) => {
|
||||
assert(["chromium", "webkit"].includes(engine), "BROWSER must be chromium or webkit");
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || "chromium" }));
|
||||
t.after(() => browser.close());
|
||||
for (const lang of ["en", "zh"]) {
|
||||
await t.test(lang, async () => {
|
||||
const w = L[lang];
|
||||
const state = {
|
||||
lib: {
|
||||
dir: `${HOME}/.magpie/library`, backups: `${HOME}/.magpie/backups`, home: HOME,
|
||||
agents: [agent("claude", "Claude Code", "claudecode-color"), agent("codex", "Codex", "openai")],
|
||||
instructions: { agents: [], sets: [] }, servers: [], foundServers: [], projects: [], foundSkills: [], problems: [],
|
||||
skills: [...LARK.map((n) => local(n, ["claude"])), ...["docx", "notes", "pdf", "web-search", "xlsx"].map((n) => local(n, ["claude"]))],
|
||||
},
|
||||
};
|
||||
const posts = [];
|
||||
const ctx = await browser.newContext({ viewport: { width: 1100, height: 760 }, reducedMotion: "reduce" });
|
||||
await ctx.addInitScript(() => { try { localStorage.setItem("magpie.libTab", "skills"); } catch {} });
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", server(lang, state, posts));
|
||||
await page.goto("http://magpie.test/");
|
||||
await page.locator('button[data-view="library"]').click();
|
||||
const v = page.locator("#view-library");
|
||||
const part = v.locator(".lib-subhead").filter({ has: page.locator(".name", { hasText: /^lark$/ }) });
|
||||
await part.waitFor();
|
||||
const row = (name) => v.locator(".lib-skill").filter({ has: page.locator(".name", { hasText: new RegExp("^" + name + "$") }) });
|
||||
const agentsOf = (name) => state.lib.skills.find((s) => s.name === name).agents;
|
||||
|
||||
// the lark part's chips: Codex is given all four, the rest are left alone
|
||||
const chip = part.locator('.lib-agents .lib-ag[data-agent="codex"]');
|
||||
assert.equal(await chip.getAttribute("aria-pressed"), "false");
|
||||
await chip.click();
|
||||
await page.locator("#status").filter({ hasText: w.part }).waitFor();
|
||||
assert.deepEqual(posts.shift(), { names: LARK, agents: ["codex"], on: true });
|
||||
assert.deepEqual(agentsOf("pdf"), ["claude"]);
|
||||
await page.waitForFunction(() => [...document.querySelectorAll("#view-library .lib-subhead")].find((h) => h.querySelector(".name")?.textContent === "lark")?.querySelector('.lib-ag[data-agent="codex"]')?.getAttribute("aria-pressed") === "true");
|
||||
assert.equal(await part.evaluate((h) => h.classList.contains("open")), true, "the click folded the part");
|
||||
|
||||
// Select: a box on each row; a row's click picks it rather than opening it
|
||||
assert.equal(await v.locator(".lib-pickbox").count(), 0);
|
||||
await v.locator(".lib-select").click();
|
||||
assert.equal((await v.locator(".lib-select").textContent()).trim(), w.done);
|
||||
await row("pdf").locator(".lib-pickbox").waitFor();
|
||||
const top = await v.evaluate((x) => x.scrollTop);
|
||||
await row("pdf").locator(".name").click();
|
||||
assert.equal(await row("pdf").locator(".lib-pickbox").isChecked(), true);
|
||||
assert.equal(await page.locator(".lib-editor").count(), 0, "the click opened the skill");
|
||||
await v.locator(".lib-pickbar .lib-pickn").filter({ hasText: w.one }).waitFor();
|
||||
await row("docx").locator(".lib-pickbox").click();
|
||||
await v.locator(".lib-pickbar .lib-pickn").filter({ hasText: w.two }).waitFor();
|
||||
assert.equal(await v.evaluate((x) => x.scrollTop), top, "a pick scrolled the page");
|
||||
|
||||
// the bar's chips: Claude Code's are taken from the two picked
|
||||
await v.locator('.lib-pickbar .lib-ag[data-agent="claude"]').click();
|
||||
await page.locator("#status").filter({ hasText: w.off }).waitFor();
|
||||
assert.deepEqual(posts.shift(), { names: ["docx", "pdf"], agents: ["claude"], on: false });
|
||||
assert.deepEqual(agentsOf("notes"), ["claude"]);
|
||||
// still picking, the two still picked
|
||||
await page.waitForFunction(() => document.querySelector('#view-library .lib-pickbar .lib-ag[data-agent="claude"]')?.getAttribute("aria-pressed") === "false");
|
||||
assert.equal(await row("docx").locator(".lib-pickbox").isChecked(), true);
|
||||
|
||||
// a heading's box picks its part; half ticked with some
|
||||
const pbox = part.locator(".lib-pickbox");
|
||||
await row("lark-doc").locator(".lib-pickbox").click();
|
||||
assert.equal(await pbox.evaluate((b) => b.indeterminate), true);
|
||||
await pbox.click();
|
||||
assert.equal(await pbox.isChecked(), true);
|
||||
for (const n of LARK) assert.equal(await row(n).locator(".lib-pickbox").isChecked(), true, n);
|
||||
await v.locator(".lib-pickbar .lib-pickn").filter({ hasText: w.six }).waitFor();
|
||||
await v.locator(".lib-pickclear").click();
|
||||
await v.locator(".lib-pickbar .lib-pickn.none").waitFor();
|
||||
|
||||
// the bar's box picks every one the filter shows
|
||||
await v.locator(".lib-skillq").fill("lark");
|
||||
await v.locator(".lib-pickall").filter({ hasText: w.match }).waitFor();
|
||||
await v.locator(".lib-pickall input").click();
|
||||
await v.locator(".lib-pickbar .lib-pickn").filter({ hasText: lang === "en" ? "4 selected" : "已选 4 个" }).waitFor();
|
||||
await v.locator('.lib-pickbar .lib-ag[data-agent="claude"]').click();
|
||||
await page.waitForFunction(() => document.querySelector('#view-library .lib-pickbar .lib-ag[data-agent="claude"]')?.getAttribute("aria-pressed") === "false");
|
||||
assert.deepEqual(posts.shift(), { names: LARK, agents: ["claude"], on: false });
|
||||
|
||||
// Done takes the boxes away; a row opens its skill again
|
||||
await v.locator(".lib-pickdone").click();
|
||||
assert.equal(await v.locator(".lib-pickbox").count(), 0);
|
||||
assert.equal(await v.locator(".lib-pickbar").count(), 0);
|
||||
assert.equal((await v.locator(".lib-select").textContent()).trim(), w.select);
|
||||
|
||||
assert.deepEqual(errors, []);
|
||||
await ctx.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user