oclaw/interfaces/admin/static/js/pages/skills.js
oliver f92cb85a4a Localize Plugins actions and parallelize Plugins/Skills boot fetches.
Cut time-to-interactive on those admin pages and align button/table copy with the existing zh/en i18n switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 19:44:07 +08:00

1584 lines
64 KiB
JavaScript

import { t, tf, el, apiGet, apiPost, assertSkillMutationOk, shortText, renderPageShell, rowActions } from "../core.js";
function skillsFold(summaryText, innerNodes) {
const det = el("details", { class: "details plugins-fold" });
det.appendChild(el("summary", { text: summaryText }));
const inner = el("div", { class: "plugins-fold__inner" });
inner.addEventListener("click", (e) => e.stopPropagation());
(innerNodes || []).forEach((n) => inner.appendChild(n));
det.appendChild(inner);
return det;
}
async function renderSkills() {
document.querySelectorAll("[data-skill-modal]").forEach((n) => n.remove());
document.querySelectorAll("body > .row-actions__menu, body > .chat-sess-menu-pop.row-actions__menu").forEach((n) => n.remove());
const SKILL_AUDIT_RETRYABLE_ONLY_KEY = "ops_admin_skill_audit_retryable_only";
const status = el("div", { class: "muted", text: "" });
const rowsState = { items: [] };
const auditState = { items: [] };
const nameInp = el("input", { class: "input", placeholder: t("skills.ph.name") });
const descInp = el("input", { class: "input", placeholder: t("skills.ph.description") });
const bodyInp = el("textarea", { class: "input", rows: "4", placeholder: t("skills.ph.body") });
const regInp = el("input", { class: "input", placeholder: t("skills.ph.registry") });
const localDirInp = el("input", { class: "input", placeholder: t("skills.ph.localDir") });
const marketStatus = el("div", { class: "muted", text: "" });
const skillModeStatus = el("div", { class: "muted", text: "" });
const skillPromptModeCb = el("input", { type: "checkbox" });
const marketQ = el("input", { class: "input", placeholder: t("skills.ph.marketSearch") });
const marketLimitInp = el("input", {
class: "input",
placeholder: t("skills.ph.limit"),
value: "40",
style: "max-width:120px;",
});
const marketTbody = el("tbody");
const marketDetailPre = el("pre", { class: "muted pre", text: "" });
let marketItems = [];
const loadSkillMode = async () => {
try {
const r = await apiGet("/admin/api/skills/mode");
skillPromptModeCb.checked = !!r.prompt_in_system;
skillModeStatus.textContent = "";
} catch (e) {
skillModeStatus.textContent = `mode: ${String(e && e.message ? e.message : e)}`;
}
};
const saveSkillMode = async () => {
skillModeStatus.textContent = `${t("chat.loading")}...`;
try {
const r = await apiPost("/admin/api/skills/mode", {
prompt_in_system: !!skillPromptModeCb.checked,
market_provider: "clawhub",
});
skillPromptModeCb.checked = !!r.prompt_in_system;
skillModeStatus.textContent = `saved: prompt=${String(!!r.prompt_in_system)} market=clawhub`;
} catch (e) {
skillModeStatus.textContent = `mode: ${String(e && e.message ? e.message : e)}`;
}
};
const loadMarket = async (q) => {
const qq = String(q || "").trim();
const lim = Math.max(1, Math.min(200, parseInt(String(marketLimitInp.value || "40"), 10) || 40));
marketStatus.textContent = `${t("chat.loading")}...`;
try {
const resp = await apiGet(
`/admin/api/skills/market/search?q=${encodeURIComponent(qq)}&limit=${encodeURIComponent(String(lim))}`,
);
marketItems = Array.isArray(resp.items) ? resp.items : [];
marketStatus.textContent = `results: ${marketItems.length}`;
repaintMarket();
} catch (e) {
marketItems = [];
marketStatus.textContent = `${t("common.error")}: ${String(e && e.message ? e.message : e)}`;
repaintMarket();
}
};
const loadMarketDetail = async (slug) => {
const s = String(slug || "").trim();
if (!s) return;
marketDetailPre.textContent = `${t("chat.loading")}...`;
try {
const resp = await apiGet(`/admin/api/skills/market/detail?slug=${encodeURIComponent(s)}`);
marketDetailPre.textContent = JSON.stringify(resp.detail || {}, null, 2);
} catch (e) {
marketDetailPre.textContent = `${t("common.error")}: ${String(e && e.message ? e.message : e)}`;
}
};
const installFromMarket = async (slug, version) => {
const s = String(slug || "").trim();
if (!s) return;
marketStatus.textContent = `installing: ${s}...`;
openSkillInstallModal(`Installing ${s}...`);
try {
const r = await apiPost("/admin/api/skills/market/install", {
slug: s,
version: version ? String(version) : undefined,
overwrite: false,
});
assertSkillMutationOk(r, "Market install failed");
status.textContent = `install-market success: ${JSON.stringify(r.result || {})}`;
marketStatus.textContent = `installed: ${s}`;
await refreshSkillsState();
finishSkillInstallModal(true, `${s} installed successfully.`);
} catch (e) {
status.textContent = String(e && e.message ? e.message : e);
marketStatus.textContent = `install failed: ${s}`;
finishSkillInstallModal(false, `Install failed: ${String(e && e.message ? e.message : e)}`);
}
};
const repaintMarket = () => {
marketTbody.innerHTML = "";
(marketItems || []).forEach((x) => {
const slug = String(x.slug || "");
const ver = String(x.version || "");
const btnDetail = el("button", {
class: "btn btn--small",
text: t("skills.action.detail"),
onclick: async () => await loadMarketDetail(slug),
});
const btnInstall = el("button", {
class: "btn btn--small btn--primary",
text: t("skills.action.install"),
onclick: async () => await installFromMarket(slug, ver),
});
marketTbody.appendChild(
el("tr", {}, [
el("td", { text: slug }),
el("td", { text: String(x.name || "") }),
el("td", { text: ver }),
el("td", { text: shortText(String(x.description || ""), 80) }),
el("td", { class: "table__cell--actions", "data-copy-disabled": "1" }, [
el("div", { class: "table__cell-actions" }, [btnInstall, btnDetail]),
]),
]),
);
});
};
const btnMarketSearch = el("button", {
class: "btn",
text: t("skills.action.search"),
onclick: async () => await loadMarket(marketQ.value),
});
const btnMarketLatest = el("button", {
class: "btn",
text: t("skills.action.latest"),
onclick: async () => await loadMarket(""),
});
// --- Binding ---
const skillBindingState = { roles: [], names: [], mapping: {}, enabled: false };
const skillBindingStatus = el("div", { class: "muted", text: "" });
const skillBindingPersistHint = el("div", {
class: "muted",
style: "margin:6px 0;line-height:1.5;",
text: t("skills.hint.bindingPersist"),
});
const skillBindingEnvHint = el("div", {
class: "muted",
style: "margin:6px 0;line-height:1.5;display:none;",
});
const skillEffectiveState = { items: [] };
const skillBindingEnabledCb = el("input", { type: "checkbox" });
const skillRoleSelect = el("select", { class: "input" }, []);
const skillBindingListWrap = el("div");
const skillBindingDashTbody = el("tbody");
const skillEffectiveTbody = el("tbody");
const renderSkillBindingDashboard = () => {
skillBindingDashTbody.innerHTML = "";
const roles = Array.isArray(skillBindingState.roles) ? skillBindingState.roles : [];
const mapping = skillBindingState.mapping && typeof skillBindingState.mapping === "object" ? skillBindingState.mapping : {};
roles.forEach((role) => {
const direct = new Set(Array.isArray(mapping[role]) ? mapping[role].map((x) => String(x)) : []);
const effective = new Set(Array.from(direct));
skillBindingDashTbody.appendChild(
el("tr", {}, [
el("td", { text: role }),
el("td", { text: String(direct.size) }),
el("td", { text: String(effective.size) }),
]),
);
});
if (!roles.length) {
skillBindingDashTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "3" })]));
}
};
const renderSkillEffectiveDashboard = () => {
skillEffectiveTbody.innerHTML = "";
const rows = Array.isArray(skillEffectiveState.items) ? skillEffectiveState.items : [];
rows.forEach((x) => {
const names = Array.isArray(x.names_preview)
? x.names_preview.map((v) => String(v || "").trim()).filter(Boolean)
: [];
const docsOnlyNames = Array.isArray(x.docs_only_names_preview)
? x.docs_only_names_preview.map((v) => String(v || "").trim()).filter(Boolean)
: [];
const previewText = names.slice(0, 6).join(", ");
const namesCell = names.length
? el("details", {}, [
el("summary", { text: previewText || "(empty)" }),
el("div", { class: "muted u-mt-4 u-wrap-lh", text: names.join(", ") }),
])
: el("span", { class: "muted", text: "-" });
const docsOnlyCell = docsOnlyNames.length
? el("details", {}, [
el("summary", { text: docsOnlyNames.slice(0, 4).join(", ") }),
el("div", { class: "muted u-mt-4 u-wrap-lh", text: docsOnlyNames.join(", ") }),
])
: el("span", { class: "muted", text: "-" });
skillEffectiveTbody.appendChild(
el("tr", {}, [
el("td", { text: String(x.role || "") }),
el("td", { text: String(x.total || 0) }),
el("td", { text: String(x.workspace_total || 0) }),
el("td", { text: String(x.workspace_direct || 0) }),
el("td", { text: String(x.workspace_resolved_tool_match || 0) }),
el("td", { text: String(x.workspace_docs_only || 0) }),
el("td", { text: String(x.mcp_total || 0) }),
el("td", { text: String(x.tool_total || 0) }),
el("td", {}, [docsOnlyCell]),
el("td", {}, [namesCell]),
]),
);
});
if (!rows.length) {
skillEffectiveTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "10" })]));
}
};
const loadSkillEffective = async () => {
try {
const r = await apiGet("/admin/api/skills/effective");
skillEffectiveState.items = Array.isArray(r.items) ? r.items : [];
renderSkillEffectiveDashboard();
} catch (e) {
skillEffectiveState.items = [];
renderSkillEffectiveDashboard();
skillBindingStatus.textContent = `effective: ${String(e && e.message ? e.message : e)}`;
}
};
const renderSkillBindingList = () => {
const current = String(skillRoleSelect.value || "");
const draft = skillBindingState.mapping;
const existing = Array.isArray(draft[current]) ? draft[current].map((x) => String(x)) : [];
const selected = new Set(existing);
skillBindingListWrap.innerHTML = "";
const skills =
skillBindingState.names.length > 0
? skillBindingState.names
: Array.isArray(rowsState.items)
? rowsState.items.map((x) => String(x.name || "").trim()).filter(Boolean)
: [];
if (!skills.length) {
skillBindingListWrap.appendChild(el("div", { class: "muted", text: t("skills.hint.noSkills") }));
return;
}
skills.forEach((nm) => {
const cb = el("input", { type: "checkbox" });
cb.checked = selected.has(nm);
cb.addEventListener("change", () => {
const prev = new Set(Array.isArray(draft[current]) ? draft[current].map((x) => String(x)) : []);
if (cb.checked) prev.add(nm);
else prev.delete(nm);
draft[current] = Array.from(prev);
});
skillBindingListWrap.appendChild(el("label", { class: "row u-row-center" }, [cb, el("span", { text: nm })]));
});
};
const applyBindingEnvHint = (r) => {
if (r && r.enabled_env_present) {
skillBindingEnvHint.style.display = "block";
const stored = !!r.enabled_stored;
const eff = !!r.enabled;
skillBindingEnvHint.textContent = tf("skills.hint.bindingEnv", {
stored: String(stored),
effective: String(eff),
});
} else {
skillBindingEnvHint.style.display = "none";
skillBindingEnvHint.textContent = "";
}
};
const loadSkillBinding = async () => {
try {
const r = await apiGet("/admin/api/skills/binding");
skillBindingState.roles = Array.isArray(r.available_roles) ? r.available_roles : [];
skillBindingState.names = (Array.isArray(r.installed_skills) ? r.installed_skills : [])
.map((x) => String(x.name || "").trim())
.filter(Boolean);
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
skillBindingState.enabled = !!r.enabled;
skillBindingEnabledCb.checked = skillBindingState.enabled;
applyBindingEnvHint(r);
skillRoleSelect.innerHTML = "";
skillBindingState.roles.forEach((role) => {
skillRoleSelect.appendChild(el("option", { value: role, text: role }));
});
if (skillBindingState.roles.length && !skillRoleSelect.value) {
skillRoleSelect.value = skillBindingState.roles[0];
}
renderSkillBindingList();
renderSkillBindingDashboard();
await loadSkillEffective();
skillBindingStatus.textContent = "";
} catch (e) {
skillBindingStatus.textContent = `binding: ${String(e && e.message ? e.message : e)}`;
skillBindingListWrap.innerHTML = "";
}
};
skillRoleSelect.addEventListener("change", () => renderSkillBindingList());
const btnSaveSkillBinding = el("button", {
class: "btn btn--primary",
text: t("skills.action.saveBinding"),
onclick: async () => {
try {
const r = await apiPost("/admin/api/skills/binding", {
enabled: !!skillBindingEnabledCb.checked,
mapping: skillBindingState.mapping,
});
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
skillBindingState.enabled = !!r.enabled;
skillBindingEnabledCb.checked = skillBindingState.enabled;
applyBindingEnvHint(r);
skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)}`;
renderSkillBindingList();
renderSkillBindingDashboard();
await loadSkillEffective();
} catch (e) {
skillBindingStatus.textContent = String(e && e.message ? e.message : e);
}
},
});
// --- Diagnostics: internal tools ---
const internalToolsState = { role: "", available_roles: [], tools: [], skipped_public: [], skipped_expert: [] };
const lazyLoadState = { internalLoaded: false, llmLoaded: false, selfCheckLoaded: false, skillHealthLoaded: false };
const internalToolsStatus = el("div", { class: "muted", text: "" });
const internalRoleSelect = el("select", { class: "input" }, []);
const internalToolsTbody = el("tbody");
const internalSkippedPre = el("pre", { class: "muted pre", text: "" });
const renderInternalTools = () => {
internalToolsTbody.innerHTML = "";
const rows = Array.isArray(internalToolsState.tools) ? internalToolsState.tools : [];
rows.forEach((x) => {
internalToolsTbody.appendChild(
el("tr", {}, [
el("td", { text: String(x.source || "") }),
el("td", { text: String(x.name || "") }),
el("td", { text: shortText(String(x.description || ""), 120) }),
el("td", { text: String((Array.isArray(x.tags) ? x.tags.join(", ") : x.tags) || "") }),
el("td", { text: String(x.read_only ? "1" : "0") }),
el("td", { text: String(x.risk_level || "") }),
]),
);
});
if (!rows.length) {
internalToolsTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "6" })]));
}
internalSkippedPre.textContent = JSON.stringify(
{
skipped_public: Array.isArray(internalToolsState.skipped_public) ? internalToolsState.skipped_public : [],
skipped_expert: Array.isArray(internalToolsState.skipped_expert) ? internalToolsState.skipped_expert : [],
},
null,
2,
);
};
const loadInternalToolsPreview = async (role) => {
const r = String(role || internalRoleSelect.value || "").trim();
if (!r) return;
internalToolsStatus.textContent = `${t("chat.loading")}...`;
try {
const resp = await apiGet(`/admin/api/tools/internal/preview?role=${encodeURIComponent(r)}`);
internalToolsState.role = String(resp.role || "");
internalToolsState.available_roles = Array.isArray(resp.available_roles) ? resp.available_roles : [];
internalToolsState.tools = Array.isArray(resp.tools) ? resp.tools : [];
internalToolsState.skipped_public = Array.isArray(resp.skipped_public) ? resp.skipped_public : [];
internalToolsState.skipped_expert = Array.isArray(resp.skipped_expert) ? resp.skipped_expert : [];
internalToolsStatus.textContent = `role=${internalToolsState.role} tools=${internalToolsState.tools.length}`;
renderInternalTools();
} catch (e) {
internalToolsState.tools = [];
internalToolsStatus.textContent = `preview: ${String(e && e.message ? e.message : e)}`;
renderInternalTools();
}
};
const btnInternalPreview = el("button", {
class: "btn btn--primary",
text: t("skills.action.preview"),
onclick: async () => await loadInternalToolsPreview(internalRoleSelect.value),
});
const internalToolsInner = [
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.internalTools") }),
internalToolsStatus,
el("div", { class: "row u-row-wrap-mt" }, [
el("label", { text: t("skills.label.role") }),
internalRoleSelect,
btnInternalPreview,
el("button", {
class: "btn",
text: t("skills.action.clearCache"),
onclick: async () => {
try {
await apiPost("/admin/api/tools/internal/reload", {});
internalToolsStatus.textContent = "cache cleared";
await loadInternalToolsPreview(internalRoleSelect.value);
} catch (e) {
internalToolsStatus.textContent = `reload: ${String(e && e.message ? e.message : e)}`;
}
},
}),
]),
el("div", { class: "table-wrap u-mt-8" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.source") }),
el("th", { text: t("skills.col.name") }),
el("th", { text: t("skills.col.description") }),
el("th", { text: t("skills.col.tags") }),
el("th", { text: t("skills.col.readOnly") }),
el("th", { text: t("skills.col.risk") }),
]),
]),
internalToolsTbody,
]),
]),
el("div", { class: "muted u-mt-8", text: t("skills.hint.skippedModules") }),
internalSkippedPre,
];
const internalToolsBox = skillsFold(t("skills.fold.internalTools"), internalToolsInner);
// --- Diagnostics: LLM tools ---
const llmToolsState = { role: "", tools_raw: [], tools_wired: [], removed_mcp_names: [], meta: {} };
const llmToolsStatus = el("div", { class: "muted", text: "" });
const llmRoleSelect = el("select", { class: "input" }, []);
const llmTracePlanCb = el("input", { type: "checkbox" });
const llmToolsTbody = el("tbody");
const llmRemovedPre = el("pre", { class: "muted pre", text: "" });
const llmModeBadge = el("span", { class: "badge badge--mode-restricted", text: "restricted" });
const llmDiffPre = el("pre", { class: "muted pre", text: "" });
const llmDiffTbody = el("tbody");
const llmDiffTableWrap = el("div", { class: "table-wrap u-mt-8" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.type") }),
el("th", { text: t("skills.col.name") }),
el("th", { text: t("skills.col.kind") }),
el("th", { text: t("skills.col.details") }),
]),
]),
llmDiffTbody,
]),
]);
// --- Diagnostics: tools self-check ---
const selfCheckStatus = el("div", { class: "muted", text: "" });
const selfCheckSummary = el("div", { class: "muted", text: "" });
const selfCheckTbody = el("tbody");
const selfCheckState = { data: null };
const renderSelfCheck = (data) => {
const body = data && typeof data === "object" ? data : {};
selfCheckState.data = body;
const summary = body.summary && typeof body.summary === "object" ? body.summary : {};
const rolesTotal = Number(body.roles_total || 0);
const totalInternal = Number(summary.total_internal_tools || 0);
const totalWired = Number(summary.total_wired_tools || 0);
const items = Array.isArray(body.items) ? body.items : [];
selfCheckSummary.textContent = `roles=${rolesTotal} internal=${totalInternal} wired=${totalWired}`;
selfCheckTbody.innerHTML = "";
items.forEach((x) => {
const removedMcp = Number(x.removed_mcp_total || 0);
const wired = Number(x.wired_count || 0);
const mode = String(x.role_mode || "restricted");
let health = "ok";
if (mode === "forbidden" && wired === 0) health = "forbidden";
else if (mode === "unrestricted") health = "unrestricted";
else if (removedMcp > 0) health = "warning";
const badgeCls =
health === "warning"
? "badge badge--mode-restricted"
: health === "forbidden"
? "badge badge--bad"
: health === "unrestricted"
? "badge badge--mode-unrestricted"
: "badge badge--ok";
selfCheckTbody.appendChild(
el("tr", {}, [
el("td", { text: String(x.role || "") }),
el("td", {}, [el("span", { class: badgeCls, text: health })]),
el("td", { text: String(x.role_mode || "") }),
el("td", { text: String(x.internal_count || 0) }),
el("td", { text: String(x.raw_count || 0) }),
el("td", { text: String(x.wired_count || 0) }),
el("td", { text: String(x.removed_mcp_total || 0) }),
]),
);
});
if (!items.length) selfCheckTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "8" })]));
};
const runSelfCheck = async () => {
selfCheckStatus.textContent = `${t("chat.loading")}...`;
try {
const resp = await apiGet("/admin/api/tools/self-check");
renderSelfCheck(resp);
selfCheckStatus.textContent = "self-check complete";
} catch (e) {
selfCheckStatus.textContent = `self-check: ${String(e && e.message ? e.message : e)}`;
renderSelfCheck({});
}
};
const exportSelfCheckJson = () => {
const data = selfCheckState.data;
if (!data || typeof data !== "object") {
selfCheckStatus.textContent = "no self-check data to export";
return;
}
try {
const blob = new Blob([`${JSON.stringify(data, null, 2)}\n`], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const a = document.createElement("a");
a.href = url;
a.download = `tools-self-check-${ts}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
selfCheckStatus.textContent = "self-check exported";
} catch (e) {
selfCheckStatus.textContent = `export failed: ${String(e && e.message ? e.message : e)}`;
}
};
const selfCheckInner = [
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.toolsSelfCheck") }),
selfCheckStatus,
el("div", { class: "row u-row-center-mt" }, [
el("button", { class: "btn btn--primary", text: t("skills.action.runSelfCheck"), onclick: runSelfCheck }),
el("button", { class: "btn", text: t("skills.action.exportSelfCheck"), onclick: exportSelfCheckJson }),
selfCheckSummary,
]),
el("div", { class: "table-wrap u-mt-8" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.role") }),
el("th", { text: t("skills.col.health") }),
el("th", { text: t("skills.col.mode") }),
el("th", { text: t("skills.col.internal") }),
el("th", { text: t("skills.col.raw") }),
el("th", { text: t("skills.col.wired") }),
el("th", { text: t("skills.col.removedMcp") }),
]),
]),
selfCheckTbody,
]),
]),
];
const selfCheckBox = skillsFold(t("skills.fold.toolsSelfCheck"), selfCheckInner);
// --- Diagnostics: skills health ---
const skillHealthStatus = el("div", { class: "muted", text: "" });
const skillHealthSummary = el("div", { class: "muted", text: "" });
const skillHealthClassifyTbody = el("tbody");
const skillHealthExecTbody = el("tbody");
const skillHealthFailedOnlyCb = el("input", { type: "checkbox" });
const skillHealthState = { data: null };
const renderSkillHealthExecRows = () => {
const body = skillHealthState.data && typeof skillHealthState.data === "object" ? skillHealthState.data : {};
const checks = Array.isArray(body.execution_checks) ? body.execution_checks : [];
const failedOnly = !!skillHealthFailedOnlyCb.checked;
skillHealthExecTbody.innerHTML = "";
checks
.filter((x) => (failedOnly ? !x.ok : true))
.slice(0, 30)
.forEach((x) => {
const ok = !!x.ok;
const code = String(x.error_code || "");
const truncated = !!x.output_truncated;
skillHealthExecTbody.appendChild(
el("tr", {}, [
el("td", { text: String(x.name || "") }),
el("td", {}, [
ok
? el("span", { class: "badge badge--ok", text: "ok" })
: el("span", { class: "badge badge--bad", text: "fail" }),
]),
el("td", { text: code || "-" }),
el("td", { text: truncated ? "1" : "0" }),
el("td", {}, [
el("button", {
class: "btn",
text: t("skills.action.testRunArgs"),
disabled: !String(x.name || "").trim(),
onclick: () => {
const nm = String(x.name || "").trim();
if (!nm) return;
openSkillTestRunModal(nm);
},
}),
]),
]),
);
});
if (!skillHealthExecTbody.children.length) {
skillHealthExecTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "5" })]));
}
};
const renderSkillHealthCheck = (data) => {
const body = data && typeof data === "object" ? data : {};
skillHealthState.data = body;
const counts = body.classification_counts && typeof body.classification_counts === "object" ? body.classification_counts : {};
const skillsTotal = Number(body.skills_total || 0);
const execTotal = Number(body.executable_total || 0);
const checked = Number(body.execution_checked_total || 0);
skillHealthSummary.textContent = `skills=${skillsTotal} executable=${execTotal} checked=${checked}`;
skillHealthClassifyTbody.innerHTML = "";
const countRows = Object.entries(counts).sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0));
countRows.forEach(([k, v]) => {
skillHealthClassifyTbody.appendChild(
el("tr", {}, [el("td", { text: String(k || "") }), el("td", { text: String(Number(v || 0)) })]),
);
});
if (!countRows.length) {
skillHealthClassifyTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "2" })]));
}
renderSkillHealthExecRows();
};
skillHealthFailedOnlyCb.addEventListener("change", () => renderSkillHealthExecRows());
const runSkillHealthCheck = async () => {
skillHealthStatus.textContent = `${t("chat.loading")}...`;
try {
const resp = await apiGet("/admin/api/skills/self-check?include_execution=true");
renderSkillHealthCheck(resp);
skillHealthStatus.textContent = "skill self-check complete";
} catch (e) {
skillHealthStatus.textContent = `skill self-check: ${String(e && e.message ? e.message : e)}`;
renderSkillHealthCheck({});
}
};
const exportSkillHealthJson = () => {
const data = skillHealthState.data;
if (!data || typeof data !== "object") {
skillHealthStatus.textContent = "no skills self-check data to export";
return;
}
try {
const blob = new Blob([`${JSON.stringify(data, null, 2)}\n`], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const a = document.createElement("a");
a.href = url;
a.download = `skills-self-check-${ts}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
skillHealthStatus.textContent = "skills self-check exported";
} catch (e) {
skillHealthStatus.textContent = `export failed: ${String(e && e.message ? e.message : e)}`;
}
};
const skillHealthInner = [
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.skillHealth") }),
skillHealthStatus,
el("div", { class: "row u-row-center-mt" }, [
el("button", {
class: "btn btn--primary",
text: t("skills.action.runSkillHealth"),
onclick: runSkillHealthCheck,
}),
el("button", {
class: "btn",
text: t("skills.action.exportSkillHealth"),
onclick: exportSkillHealthJson,
}),
el("label", { class: "row u-row-center" }, [
skillHealthFailedOnlyCb,
el("span", { class: "muted", text: t("skills.action.failedOnly") }),
]),
skillHealthSummary,
]),
el("div", { class: "row", style: "gap:12px;align-items:flex-start;flex-wrap:wrap;margin-top:8px;" }, [
el("div", { class: "table-wrap", style: "min-width:280px;flex:1;" }, [
el("div", { class: "muted u-mb-6", text: "classification_counts" }),
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [el("th", { text: t("skills.col.code") }), el("th", { text: t("skills.col.count") })]),
]),
skillHealthClassifyTbody,
]),
]),
el("div", { class: "table-wrap", style: "min-width:360px;flex:2;" }, [
el("div", { class: "muted u-mb-6", text: "execution_checks (top 30)" }),
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.skill") }),
el("th", { text: t("skills.col.status") }),
el("th", { text: t("skills.col.errorCode") }),
el("th", { text: t("skills.col.outputTruncated") }),
el("th", { text: t("skills.col.action") }),
]),
]),
skillHealthExecTbody,
]),
]),
]),
];
const skillHealthBox = skillsFold(t("skills.fold.health"), skillHealthInner);
const _renderLLMToolRows = (tools) => {
llmToolsTbody.innerHTML = "";
const rows = Array.isArray(tools) ? tools : [];
rows.forEach((ent) => {
const fn = ent && typeof ent === "object" ? ent.function : null;
const name = fn && typeof fn === "object" ? String(fn.name || "") : "";
const desc = fn && typeof fn === "object" ? String(fn.description || "") : "";
const params = fn && typeof fn === "object" ? fn.parameters : null;
llmToolsTbody.appendChild(
el("tr", {}, [
el("td", { text: name }),
el("td", { text: shortText(desc, 140) }),
el("td", { text: name.startsWith("mcp__") ? "mcp" : "internal" }),
el("td", { text: params ? shortText(JSON.stringify(params), 120) : "" }),
]),
);
});
if (!rows.length) {
llmToolsTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "4" })]));
}
};
const loadLLMToolsPreview = async (role) => {
const r = String(role || llmRoleSelect.value || "").trim();
if (!r) return;
llmToolsStatus.textContent = `${t("chat.loading")}...`;
try {
const resp = await apiGet(`/admin/api/tools/llm/preview?role=${encodeURIComponent(r)}`);
llmToolsState.role = String(resp.role || "");
llmToolsState.tools_raw = Array.isArray(resp.tools_raw) ? resp.tools_raw : [];
llmToolsState.tools_wired = Array.isArray(resp.tools_wired) ? resp.tools_wired : [];
llmToolsState.removed_mcp_names = Array.isArray(resp.removed_mcp_names) ? resp.removed_mcp_names : [];
llmToolsState.meta = resp && typeof resp === "object" ? resp : {};
const mode = String(resp.role_mode || "restricted");
llmModeBadge.textContent = mode;
llmModeBadge.className = `badge badge--mode-${mode}`;
llmToolsStatus.textContent = `role=${llmToolsState.role} raw=${llmToolsState.tools_raw.length} wired=${llmToolsState.tools_wired.length} mcp_enabled=${String(!!resp.mcp_enabled)}`;
llmRemovedPre.textContent = JSON.stringify(
{
removed_mcp_names: llmToolsState.removed_mcp_names,
role_mode: resp.role_mode,
skipped_public: resp.skipped_public || [],
skipped_expert: resp.skipped_expert || [],
},
null,
2,
);
const _toMap = (arr) => {
const m = new Map();
(Array.isArray(arr) ? arr : []).forEach((ent) => {
const fn = ent && typeof ent === "object" ? ent.function : null;
const nm = fn && typeof fn === "object" ? String(fn.name || "") : "";
if (!nm) return;
const desc = fn && typeof fn === "object" ? String(fn.description || "") : "";
const params = fn && typeof fn === "object" ? fn.parameters : null;
m.set(nm, { description: desc, parameters: params });
});
return m;
};
const rawMap = _toMap(llmToolsState.tools_raw);
const wiredMap = _toMap(llmToolsState.tools_wired);
const rawNames = Array.from(rawMap.keys());
const wiredNames = Array.from(wiredMap.keys());
const rawSet = new Set(rawNames);
const wiredSet = new Set(wiredNames);
const removed = rawNames.filter((n) => !wiredSet.has(n)).sort();
const added = wiredNames.filter((n) => !rawSet.has(n)).sort();
const changed = [];
wiredNames.forEach((n) => {
if (!rawSet.has(n)) return;
const a = rawMap.get(n) || {};
const b = wiredMap.get(n) || {};
const d0 = String(a.description || "");
const d1 = String(b.description || "");
const p0 = a.parameters ? JSON.stringify(a.parameters) : "";
const p1 = b.parameters ? JSON.stringify(b.parameters) : "";
if (d0 !== d1 || p0 !== p1) changed.push(n);
});
llmDiffPre.textContent = JSON.stringify(
{
added,
removed,
changed,
counts: {
raw: rawNames.length,
wired: wiredNames.length,
added: added.length,
removed: removed.length,
changed: changed.length,
},
},
null,
2,
);
llmDiffTbody.innerHTML = "";
const _kind = (nm) => (String(nm || "").startsWith("mcp__") ? "mcp" : "internal");
const _pushRow = (tp, nm, detailEl) => {
llmDiffTbody.appendChild(
el("tr", {}, [
el("td", { text: tp }),
el("td", { text: nm }),
el("td", { text: _kind(nm) }),
el("td", {}, [detailEl]),
]),
);
};
const _detailsCompare = (nm) => {
const a = rawMap.get(nm) || {};
const b = wiredMap.get(nm) || {};
const d0 = String(a.description || "");
const d1 = String(b.description || "");
const p0 = a.parameters ? JSON.stringify(a.parameters, null, 2) : "";
const p1 = b.parameters ? JSON.stringify(b.parameters, null, 2) : "";
return el("details", {}, [
el("summary", { class: "u-drag", text: "compare" }),
el("div", { class: "muted u-mt-6" }, [el("div", { text: "raw.description" }), el("pre", { class: "muted pre", text: d0 })]),
el("div", { class: "muted u-mt-6" }, [el("div", { text: "wired.description" }), el("pre", { class: "muted pre", text: d1 })]),
el("div", { class: "muted u-mt-6" }, [el("div", { text: "raw.parameters" }), el("pre", { class: "muted pre", text: p0 })]),
el("div", { class: "muted u-mt-6" }, [el("div", { text: "wired.parameters" }), el("pre", { class: "muted pre", text: p1 })]),
]);
};
added.forEach((nm) => _pushRow("added", nm, el("span", { class: "muted", text: "present in wired only" })));
removed.forEach((nm) => _pushRow("removed", nm, el("span", { class: "muted", text: "present in raw only" })));
changed.sort().forEach((nm) => _pushRow("changed", nm, _detailsCompare(nm)));
if (!added.length && !removed.length && !changed.length) {
llmDiffTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "4" })]));
}
_renderLLMToolRows(llmToolsState.tools_wired);
} catch (e) {
llmToolsState.tools_wired = [];
llmToolsStatus.textContent = `preview: ${String(e && e.message ? e.message : e)}`;
llmRemovedPre.textContent = "";
llmDiffPre.textContent = "";
llmDiffTbody.innerHTML = "";
_renderLLMToolRows([]);
}
};
const btnLLMPreview = el("button", {
class: "btn btn--primary",
text: t("skills.action.preview"),
onclick: async () => await loadLLMToolsPreview(llmRoleSelect.value),
});
const loadExposureTraceSetting = async () => {
try {
const resp = await apiGet("/admin/api/tools/exposure-trace-setting");
llmTracePlanCb.checked = !!(resp && resp.enabled);
} catch (_) {
llmTracePlanCb.checked = false;
}
};
const saveExposureTraceSetting = async () => {
try {
const resp = await apiPost("/admin/api/tools/exposure-trace-setting", { enabled: !!llmTracePlanCb.checked });
llmTracePlanCb.checked = !!(resp && resp.enabled);
llmToolsStatus.textContent = `trace_exposure_plan=${llmTracePlanCb.checked ? "on" : "off"}`;
} catch (e) {
llmToolsStatus.textContent = `trace setting: ${String(e && e.message ? e.message : e)}`;
}
};
const btnLLMToggle = el("button", {
class: "btn",
text: t("skills.action.toggleRawWired"),
onclick: () => {
const showing = String(btnLLMToggle.dataset.showing || "wired");
const next = showing === "wired" ? "raw" : "wired";
btnLLMToggle.dataset.showing = next;
_renderLLMToolRows(next === "wired" ? llmToolsState.tools_wired : llmToolsState.tools_raw);
},
});
const llmToolsInner = [
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.llmTools") }),
llmToolsStatus,
el("div", { class: "row u-row-wrap-mt" }, [
el("label", { text: t("skills.label.role") }),
llmRoleSelect,
btnLLMPreview,
btnLLMToggle,
el("button", {
class: "btn",
text: t("skills.action.clearCache"),
onclick: async () => {
try {
await apiPost("/admin/api/tools/internal/reload", {});
llmToolsStatus.textContent = "cache cleared";
await loadLLMToolsPreview(llmRoleSelect.value);
} catch (e) {
llmToolsStatus.textContent = `reload: ${String(e && e.message ? e.message : e)}`;
}
},
}),
el("span", { class: "muted", text: "role_mode:" }),
llmModeBadge,
el("label", { class: "row u-row-center" }, [
llmTracePlanCb,
el("span", { class: "muted", text: t("skills.action.traceExposure") }),
]),
el("button", {
class: "btn",
text: t("skills.action.saveTrace"),
onclick: saveExposureTraceSetting,
}),
]),
el("div", { class: "table-wrap u-mt-8" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.name") }),
el("th", { text: t("skills.col.description") }),
el("th", { text: t("skills.col.kind") }),
el("th", { text: t("skills.col.parameters") }),
]),
]),
llmToolsTbody,
]),
]),
el("div", { class: "muted u-mt-8", text: t("skills.hint.diffDiag") }),
llmRemovedPre,
el("div", { class: "muted u-mt-8", text: t("skills.hint.rawWiredDiff") }),
llmDiffPre,
llmDiffTableWrap,
];
const llmToolsBox = skillsFold(t("skills.fold.llmTools"), llmToolsInner);
// --- Installed table + audit + modals ---
const tbody = el("tbody");
const auditTbody = el("tbody");
let skillTestRunName = "";
const skillTestRunModal = el("div", { class: "session-monitor-modal u-hidden", "data-skill-modal": "testrun" });
const skillTestRunTitle = el("div", { class: "card__title", text: t("skills.modal.testRun") });
const skillTestRunArgs = el("textarea", {
class: "input",
rows: "8",
style: "width:100%;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;",
placeholder: "{}",
});
const skillInstallModal = el("div", { class: "session-monitor-modal u-hidden", "data-skill-modal": "install" });
const skillInstallTitle = el("div", { class: "card__title", text: t("skills.modal.install") });
const skillInstallMsg = el("div", { class: "muted", style: "line-height:1.5;" });
const closeSkillInstallModal = () => {
skillInstallModal.classList.add("u-hidden");
skillInstallModal.style.display = "";
};
const openSkillInstallModal = (msg) => {
skillInstallTitle.textContent = t("skills.modal.install");
skillInstallMsg.textContent = String(msg || "");
if (skillInstallModal.parentNode !== document.body) document.body.appendChild(skillInstallModal);
skillInstallModal.classList.remove("u-hidden");
skillInstallModal.style.display = "";
};
const finishSkillInstallModal = (ok, msg) => {
skillInstallTitle.textContent = ok ? t("skills.modal.installOk") : t("skills.modal.installFail");
skillInstallMsg.textContent = String(msg || "");
setTimeout(() => {
closeSkillInstallModal();
}, 1200);
};
skillInstallModal.addEventListener("click", (e) => {
if (e.target === skillInstallModal) closeSkillInstallModal();
});
skillInstallModal.appendChild(
el("div", { class: "card session-monitor-modal__card", style: "width:min(560px,96vw);" }, [
skillInstallTitle,
skillInstallMsg,
el("div", { class: "row u-row-end" }, [
el("button", { class: "btn", text: t("skills.action.close"), onclick: closeSkillInstallModal }),
]),
]),
);
const closeSkillTestRunModal = () => {
skillTestRunModal.classList.add("u-hidden");
skillTestRunModal.style.display = "";
skillTestRunName = "";
};
const openSkillTestRunModal = (name) => {
skillTestRunName = String(name || "");
skillTestRunTitle.textContent = tf("skills.modal.testRunNamed", { name: skillTestRunName });
skillTestRunArgs.value = "{}";
if (skillTestRunModal.parentNode !== document.body) document.body.appendChild(skillTestRunModal);
skillTestRunModal.classList.remove("u-hidden");
skillTestRunModal.style.display = "";
setTimeout(() => skillTestRunArgs.focus(), 0);
};
skillTestRunModal.addEventListener("click", (e) => {
if (e.target === skillTestRunModal) closeSkillTestRunModal();
});
const skillTestRunRunBtn = el("button", {
class: "btn btn--primary",
text: t("skills.action.run"),
onclick: async () => {
if (!skillTestRunName) return;
let args = {};
try {
const raw = String(skillTestRunArgs.value || "").trim();
args = raw ? JSON.parse(raw) : {};
} catch (e) {
status.textContent = `invalid json: ${String(e && e.message ? e.message : e)}`;
return;
}
try {
const r = await apiPost("/admin/api/skills/test-run", { name: skillTestRunName, args });
status.textContent = `test-run: ${skillTestRunName} => ${JSON.stringify((r && r.result) || {}, null, 0)}`;
closeSkillTestRunModal();
} catch (e) {
status.textContent = `test-run: ${String(e && e.message ? e.message : e)}`;
}
},
});
const skillTestRunCancelBtn = el("button", {
class: "btn",
text: t("skills.action.cancel"),
onclick: closeSkillTestRunModal,
});
skillTestRunModal.appendChild(
el("div", { class: "card session-monitor-modal__card", style: "width:min(760px,96vw);" }, [
skillTestRunTitle,
el("div", { class: "muted", style: "margin-bottom:8px;", text: t("skills.hint.testRunArgs") }),
skillTestRunArgs,
el("div", { class: "row u-row-end" }, [skillTestRunCancelBtn, skillTestRunRunBtn]),
]),
);
const retryableOnlyCb = el("input", { type: "checkbox" });
const retryableOnlyStored = String(localStorage.getItem(SKILL_AUDIT_RETRYABLE_ONLY_KEY) || "")
.trim()
.toLowerCase();
retryableOnlyCb.checked = retryableOnlyStored
? retryableOnlyStored !== "0" && retryableOnlyStored !== "false"
: true;
const _parseSourceFromAudit = (it) => {
const d = it && typeof it.detail === "object" ? it.detail : {};
const s = String(d.source || "").trim().toLowerCase();
if (s === "local" || s === "registry" || s === "auto") return s;
return "";
};
const _parseRetryTargetFromAudit = (it) => {
const d = it && typeof it.detail === "object" ? it.detail : {};
const inputTarget = String(d.input_target || "").trim();
if (inputTarget) return inputTarget;
return String((it && it.target_id) || "").trim();
};
const _isRetryableAudit = (it) => {
const action = String((it && it.action) || "").trim().toLowerCase();
if (action !== "skill_install_failed") return false;
const src = _parseSourceFromAudit(it);
const target = _parseRetryTargetFromAudit(it);
return Boolean(src && target);
};
const loadRows = async () => {
const data = await apiGet("/admin/api/skills");
rowsState.items = Array.isArray(data.items) ? data.items : [];
};
const loadAudits = async () => {
const data = await apiGet("/admin/api/admin-audit?limit=80");
const items = Array.isArray(data.items) ? data.items : [];
auditState.items = items.filter((x) => String(x.action || "").startsWith("skill_")).slice(0, 20);
};
const updateInstalledFoldTitle = () => {
if (foldInstalledSummary) {
foldInstalledSummary.textContent = tf("skills.fold.installed", { n: (rowsState.items || []).length });
}
};
let foldInstalledSummary = null;
const repaint = () => {
const rows = rowsState.items || [];
tbody.innerHTML = "";
rows.forEach((x) => {
const name = String(x.name || "");
const enabled = !!x.enabled;
const executable = !!x.executable;
const runtime = x && typeof x.runtime === "object" ? x.runtime : null;
const runtimeType = runtime && typeof runtime.type === "string" ? String(runtime.type) : "";
const runtimeEntry = runtime && typeof runtime.entry === "string" ? String(runtime.entry) : "";
const actions = rowActions("⋯", [
{
label: enabled ? t("skills.action.disable") : t("skills.action.enable"),
onClick: async () => {
await apiPost(enabled ? "/admin/api/skills/disable" : "/admin/api/skills/enable", { name });
status.textContent = `${enabled ? "Disabled" : "Enabled"}: ${name}`;
await loadRows();
repaint();
},
},
{
label: t("skills.action.testRun"),
disabled: !executable,
onClick: () => openSkillTestRunModal(name),
},
{
label: t("skills.action.repairDeps"),
onClick: async () => {
status.textContent = `repair deps: ${name}...`;
const r = await apiPost("/admin/api/skills/repair-deps", { name });
assertSkillMutationOk(r, "Repair deps failed");
status.textContent = `repair deps: ${JSON.stringify((r && r.result) || {}, null, 0)}`;
await loadRows();
await loadAudits();
repaint();
},
},
{
label: t("skills.action.uninstall"),
danger: true,
onClick: async () => {
if (!confirm(tf("skills.confirm.uninstall", { name }))) return;
status.textContent = `uninstalling: ${name}...`;
const r = await apiPost("/admin/api/skills/uninstall", { name });
assertSkillMutationOk(r, "Uninstall failed");
status.textContent = `uninstall success: ${JSON.stringify((r && r.result) || {}, null, 0)}`;
await loadRows();
await loadAudits();
repaint();
},
},
]);
tbody.appendChild(
el("tr", {}, [
el("td", { text: name }),
el("td", { text: String(x.description || "") }),
el("td", { text: String(x.enabled ? "1" : "0") }),
el("td", {}, [
executable
? el("span", { class: "badge badge--ok", text: `${runtimeType || "runtime"}` })
: el("span", { class: "muted", text: "docs-only" }),
]),
el("td", { text: executable ? `${runtimeEntry}` : "-" }),
el("td", { text: String(x.skill_dir || "") }),
el("td", { class: "table__cell--actions", "data-copy-disabled": "1" }, [
el("div", { class: "table__cell-actions" }, [actions]),
]),
]),
);
});
updateInstalledFoldTitle();
auditTbody.innerHTML = "";
const auditRows = (auditState.items || []).filter((x) => (!retryableOnlyCb.checked ? true : _isRetryableAudit(x)));
for (const x of auditRows) {
const src = _parseSourceFromAudit(x);
const isRetryable = _isRetryableAudit(x);
const retryBtn = el("button", {
class: "btn",
text: t("skills.action.retry"),
disabled: !isRetryable,
onclick: async () => {
try {
const target = _parseRetryTargetFromAudit(x);
if (!src || !target || !isRetryable) {
status.textContent = "retry skipped: missing source/target";
return;
}
const r = await apiPost("/admin/api/skills/retry-install", { source: src, target });
assertSkillMutationOk(r, "Retry install failed");
status.textContent = `retry: ${JSON.stringify(r.result || {})}`;
await loadRows();
await loadAudits();
repaint();
} catch (e) {
status.textContent = String(e && e.message ? e.message : e);
}
},
});
auditTbody.appendChild(
el("tr", {}, [
el("td", { text: String(x.timestamp || "") }),
el("td", { text: String(x.action || "") }),
el("td", { text: String(x.target_id || "") }),
el("td", { text: String(x.status || "") }),
el("td", { text: shortText(JSON.stringify(x.detail || {}), 180) }),
el("td", {}, [retryBtn]),
]),
);
}
};
const refreshSkillsState = async () => {
await Promise.all([loadRows(), loadAudits(), loadSkillBinding()]);
repaint();
renderSkillBindingList();
};
try {
await Promise.all([loadRows(), loadAudits(), loadSkillBinding(), loadSkillMode()]);
const rolesForPreview = (
Array.isArray(skillBindingState.roles) && skillBindingState.roles.length
? skillBindingState.roles
: ["generalist", "ops", "memory"]
).map((x) => String(x));
internalRoleSelect.innerHTML = "";
rolesForPreview.forEach((role) => internalRoleSelect.appendChild(el("option", { value: role, text: role })));
internalRoleSelect.value = rolesForPreview.includes("generalist") ? "generalist" : rolesForPreview[0];
internalToolsStatus.textContent = t("skills.hint.expandToLoad");
llmRoleSelect.innerHTML = "";
rolesForPreview.forEach((role) => llmRoleSelect.appendChild(el("option", { value: role, text: role })));
llmRoleSelect.value = internalRoleSelect.value;
llmToolsStatus.textContent = t("skills.hint.expandToLoad");
selfCheckStatus.textContent = t("skills.hint.expandToSelfCheck");
skillHealthStatus.textContent = t("skills.hint.expandToSelfCheck");
} catch (e) {
return el("div", { class: "card" }, [
el("div", { class: "card__title", text: t("skills.title") }),
el("div", { class: "muted", text: String(e && e.message ? e.message : e) }),
]);
}
repaint();
const btnCreate = el("button", {
class: "btn btn--primary",
text: t("skills.action.create"),
onclick: async () => {
try {
const r = await apiPost("/admin/api/skills/create", {
name: String(nameInp.value || "").trim(),
description: String(descInp.value || "").trim(),
body_markdown: String(bodyInp.value || ""),
});
assertSkillMutationOk(r, "Create skill failed");
status.textContent = `create: ${JSON.stringify(r.result || {})}`;
await loadRows();
await loadAudits();
repaint();
} catch (e) {
status.textContent = String(e && e.message ? e.message : e);
}
},
});
const btnInstallRegistry = el("button", {
class: "btn",
text: t("skills.action.installRegistry"),
onclick: async () => {
const prev = btnInstallRegistry.textContent;
btnInstallRegistry.disabled = true;
btnInstallRegistry.textContent = t("skills.action.installing");
status.textContent = "installing from registry...";
openSkillInstallModal("Installing skill from registry...");
try {
const r = await apiPost("/admin/api/skills/install-registry", {
archive_url: String(regInp.value || "").trim(),
});
assertSkillMutationOk(r, "Registry install failed");
status.textContent = `install-registry success: ${JSON.stringify(r.result || {})}`;
await refreshSkillsState();
finishSkillInstallModal(true, "Registry skill installed successfully.");
} catch (e) {
status.textContent = String(e && e.message ? e.message : e);
finishSkillInstallModal(false, `Install failed: ${String(e && e.message ? e.message : e)}`);
} finally {
btnInstallRegistry.disabled = false;
btnInstallRegistry.textContent = prev;
}
},
});
const btnInstallLocal = el("button", {
class: "btn",
text: t("skills.action.installLocal"),
onclick: async () => {
const prev = btnInstallLocal.textContent;
btnInstallLocal.disabled = true;
btnInstallLocal.textContent = t("skills.action.installing");
status.textContent = "installing from local dir...";
openSkillInstallModal("Installing skill from local directory...");
try {
const r = await apiPost("/admin/api/skills/install", {
source_dir: String(localDirInp.value || "").trim(),
});
assertSkillMutationOk(r, "Local install failed");
status.textContent = `install-local success: ${JSON.stringify(r.result || {})}`;
await refreshSkillsState();
finishSkillInstallModal(true, "Local skill installed successfully.");
} catch (e) {
status.textContent = String(e && e.message ? e.message : e);
finishSkillInstallModal(false, `Install failed: ${String(e && e.message ? e.message : e)}`);
} finally {
btnInstallLocal.disabled = false;
btnInstallLocal.textContent = prev;
}
},
});
const btnRefresh = el("button", {
class: "btn",
text: t("skills.action.refresh"),
onclick: async () => {
try {
await refreshSkillsState();
status.textContent = "refreshed";
} catch (e) {
status.textContent = String(e && e.message ? e.message : e);
}
},
});
const btnRepairDepsAll = el("button", {
class: "btn",
text: t("skills.action.repairAllDeps"),
onclick: async () => {
const prev = btnRepairDepsAll.textContent;
btnRepairDepsAll.disabled = true;
btnRepairDepsAll.textContent = t("skills.action.repairing");
try {
const r = await apiPost("/admin/api/skills/repair-deps-all", {});
const s = r && typeof r.summary === "object" ? r.summary : {};
status.textContent = `repair all deps: total=${Number(s.total || 0)} ok=${Number(s.ok_count || 0)} warn=${Number(s.warn_count || 0)} fail=${Number(s.fail_count || 0)}`;
await refreshSkillsState();
} catch (e) {
status.textContent = `repair all deps failed: ${String(e && e.message ? e.message : e)}`;
} finally {
btnRepairDepsAll.disabled = false;
btnRepairDepsAll.textContent = prev;
}
},
});
retryableOnlyCb.addEventListener("change", () => {
localStorage.setItem(SKILL_AUDIT_RETRYABLE_ONLY_KEY, retryableOnlyCb.checked ? "1" : "0");
repaint();
});
const docsHint = el("div", { class: "muted", style: "margin-bottom:10px;line-height:1.5;" }, [
el("div", { text: t("skills.docsTitle") }),
el("div", { text: t("skills.docsTroubleshooting") }),
el("div", { text: t("skills.docsTraceTaxonomy") }),
]);
internalToolsBox.addEventListener("toggle", async () => {
if (!internalToolsBox.open || lazyLoadState.internalLoaded) return;
lazyLoadState.internalLoaded = true;
await loadInternalToolsPreview(internalRoleSelect.value);
});
llmToolsBox.addEventListener("toggle", async () => {
if (!llmToolsBox.open || lazyLoadState.llmLoaded) return;
lazyLoadState.llmLoaded = true;
await Promise.all([loadExposureTraceSetting(), loadLLMToolsPreview(llmRoleSelect.value)]);
});
selfCheckBox.addEventListener("toggle", async () => {
if (!selfCheckBox.open || lazyLoadState.selfCheckLoaded) return;
lazyLoadState.selfCheckLoaded = true;
await runSelfCheck();
});
skillHealthBox.addEventListener("toggle", async () => {
if (!skillHealthBox.open || lazyLoadState.skillHealthLoaded) return;
lazyLoadState.skillHealthLoaded = true;
await runSkillHealthCheck();
});
// --- Page folds ---
const foldMode = skillsFold(t("skills.fold.mode"), [
el("div", { class: "muted", text: t("skills.fold.modeHint") }),
el("div", { class: "row", style: "gap:8px;align-items:center;flex-wrap:wrap;" }, [
el("label", { class: "row u-row-center" }, [
skillPromptModeCb,
el("span", { text: t("skills.label.promptMode") }),
]),
el("button", { class: "btn btn--primary", text: t("skills.action.save"), onclick: saveSkillMode }),
skillModeStatus,
]),
docsHint,
]);
const foldMarket = skillsFold(t("skills.fold.market"), [
el("div", { class: "row u-row-wrap-mb" }, [marketQ, marketLimitInp, btnMarketSearch, btnMarketLatest]),
marketStatus,
el("div", { class: "u-h-8" }),
el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.slug") }),
el("th", { text: t("skills.col.name") }),
el("th", { text: t("skills.col.version") }),
el("th", { text: t("skills.col.description") }),
el("th", { text: t("skills.col.action") }),
]),
]),
marketTbody,
]),
]),
el("div", { class: "u-h-8" }),
el("div", { class: "muted", text: t("skills.action.detail") }),
marketDetailPre,
]);
const foldLocalRegistry = skillsFold(t("skills.fold.localRegistry"), [
el("div", { class: "muted", style: "margin-bottom:8px;line-height:1.5;" }, [
el("div", { text: t("skills.hintLocalDir") }),
el("div", { text: t("skills.hintRegistry") }),
]),
el("div", { class: "row u-row-wrap-mb" }, [regInp, btnInstallRegistry]),
el("div", { class: "row u-row-wrap-mb" }, [localDirInp, btnInstallLocal]),
]);
const foldCreate = skillsFold(t("skills.fold.create"), [
el("div", { class: "row u-row-wrap-mb" }, [nameInp, descInp]),
el("div", { style: "margin-bottom:8px;" }, [bodyInp]),
el("div", { class: "row u-row-wrap-mb" }, [btnCreate]),
]);
const foldInstall = skillsFold(t("skills.fold.install"), [
el("div", { class: "muted", text: t("skills.fold.installHint") }),
foldMarket,
foldLocalRegistry,
foldCreate,
]);
foldMarket.open = true;
const foldAudit = skillsFold(t("skills.fold.audit"), [
el("div", { class: "row", style: "gap:8px;align-items:center;" }, [
el("label", { class: "row u-row-center" }, [
retryableOnlyCb,
el("span", { class: "muted", text: t("skills.action.retryableOnly") }),
]),
]),
el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.timestamp") }),
el("th", { text: t("skills.col.action") }),
el("th", { text: t("skills.col.target") }),
el("th", { text: t("skills.col.status") }),
el("th", { text: t("skills.col.detail") }),
el("th", { text: t("skills.col.retry") }),
]),
]),
auditTbody,
]),
]),
]);
const foldInstalled = skillsFold(tf("skills.fold.installed", { n: (rowsState.items || []).length }), [
el("div", { class: "row u-row-wrap-mb" }, [btnRefresh, btnRepairDepsAll]),
status,
el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.name") }),
el("th", { text: t("skills.col.description") }),
el("th", { text: t("skills.col.enabled") }),
el("th", { text: t("skills.col.kind") }),
el("th", { text: t("skills.col.runtimeEntry") }),
el("th", { text: t("skills.col.path") }),
el("th", { class: "table__cell--actions", text: t("skills.col.action") }),
]),
]),
tbody,
]),
]),
foldAudit,
]);
foldInstalled.open = true;
foldInstalledSummary = foldInstalled.querySelector("summary");
const foldEffective = skillsFold(t("skills.fold.effective"), [
el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.role") }),
el("th", { text: t("skills.col.totalEffective") }),
el("th", { text: t("skills.col.workspace") }),
el("th", { text: t("skills.col.directBind") }),
el("th", { text: t("skills.col.workspaceResolved") }),
el("th", { text: t("skills.col.workspaceDocsOnly") }),
el("th", { text: t("skills.col.mcpConverted") }),
el("th", { text: t("skills.col.otherTools") }),
el("th", { text: t("skills.col.docsOnlyNames") }),
el("th", { text: t("skills.col.namesPreview") }),
]),
]),
skillEffectiveTbody,
]),
]),
]);
const foldBindingDash = skillsFold(t("skills.fold.bindingDash"), [
el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [
el("thead", {}, [
el("tr", {}, [
el("th", { text: t("skills.col.role") }),
el("th", { text: t("skills.col.directSkills") }),
el("th", { text: t("skills.col.effective") }),
]),
]),
skillBindingDashTbody,
]),
]),
foldEffective,
]);
const foldBinding = skillsFold(t("skills.fold.binding"), [
el("div", { class: "muted", text: t("skills.fold.bindingHint") }),
skillBindingStatus,
skillBindingPersistHint,
skillBindingEnvHint,
el("label", { class: "row", style: "gap:8px;align-items:center;margin-top:6px;" }, [
skillBindingEnabledCb,
el("span", { text: t("skills.label.enableBinding") }),
]),
el("div", { class: "row u-row-wrap-mt" }, [
el("label", { text: t("skills.label.role") }),
skillRoleSelect,
btnSaveSkillBinding,
]),
skillBindingListWrap,
foldBindingDash,
]);
const foldDiagnostics = skillsFold(t("skills.fold.diagnostics"), [
skillHealthBox,
selfCheckBox,
internalToolsBox,
llmToolsBox,
]);
return renderPageShell(
{
title: t("skills.title"),
subtitle: t("skills.subtitle"),
sections: [
{ id: "skills-mode", label: t("skills.toc.mode") },
{ id: "skills-install", label: t("skills.toc.install") },
{ id: "skills-installed", label: t("skills.toc.installed") },
{ id: "skills-binding", label: t("skills.toc.binding") },
{ id: "skills-diagnostics", label: t("skills.toc.diagnostics") },
],
},
[
el("div", { class: "page-grid page-grid--single" }, [
el("div", { id: "skills-mode" }, [foldMode]),
el("div", { id: "skills-install" }, [foldInstall]),
el("div", { id: "skills-installed" }, [foldInstalled]),
el("div", { id: "skills-binding" }, [foldBinding]),
el("div", { id: "skills-diagnostics" }, [foldDiagnostics]),
]),
],
);
}
export { skillsFold, renderSkills };