agent / plugins /_plugin_validator /webui /plugin-validator-store.js
GraziePrego's picture
Upload folder using huggingface_hub
7d4338a verified
import { marked } from "/vendor/marked/marked.esm.js";
import { createStore } from "/js/AlpineStore.js";
import * as api from "/js/api.js";
import { openModal as openAppModal } from "/js/modals.js";
import { toastFrontendError } from "/components/notifications/notification-store.js";
const BASE = "/plugins/_plugin_validator/webui";
let _config = null;
let _templateCache = null;
let _guidanceCache = null;
let _pollGen = 0;
let _queue = [];
let _running = null;
const POLL_INTERVAL = 2000;
const MAX_POLL_MS = 10 * 60 * 1000;
async function fetchText(url, label) {
const response = await fetch(url);
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(`Failed to load ${label}: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`);
}
return response.text();
}
async function fetchJson(url, label) {
const response = await fetch(url);
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(`Failed to load ${label}: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`);
}
return response.json();
}
async function loadConfig() {
if (_config) return _config;
try {
_config = await fetchJson(`${BASE}/plugin-validator-checks.json`, "validator checks");
return _config;
} catch (error) {
_config = null;
throw error;
}
}
async function loadTemplate() {
if (_templateCache) return _templateCache;
try {
_templateCache = await fetchText(`${BASE}/plugin-validator-prompt.md`, "validator prompt template");
return _templateCache;
} catch (error) {
_templateCache = null;
throw error;
}
}
async function loadGuidance() {
if (_guidanceCache) return _guidanceCache;
try {
_guidanceCache = await fetchText(`${BASE}/plugin-validator-guidance.md`, "validator guidance");
return _guidanceCache;
} catch (error) {
_guidanceCache = null;
throw error;
}
}
function formatCriteria(ratings, criteria) {
return Object.entries(criteria)
.map(([level, desc]) => `- ${ratings[level].icon} ${desc}`)
.join("\n");
}
function formatStatusLegend(ratings) {
return Object.entries(ratings)
.map(([, rating]) => `- ${rating.icon} **${rating.label}**`)
.join("\n");
}
function formatRatingIcons(ratings) {
return Object.values(ratings).map((rating) => rating.icon).join("/");
}
function sourceLabel(source) {
return {
local: "Local Plugin",
git: "Git Repository",
zip: "Uploaded ZIP",
}[source] || "Plugin Source";
}
function sanitizeTarget(value) {
return String(value || "").trim().replaceAll("{", "(").replaceAll("}", ")");
}
function targetReference(source, state, overrideTarget = "") {
if (overrideTarget) return sanitizeTarget(overrideTarget);
if (source === "git") {
return sanitizeTarget(state.gitUrl) || "<paste git URL here>";
}
if (source === "zip") {
return state.zipFileName
? `<uploaded ZIP: ${sanitizeTarget(state.zipFileName)}>`
: "<uploaded ZIP will be extracted for validation>";
}
return state.localPluginName
? `usr/plugins/${sanitizeTarget(state.localPluginName)}/`
: "<select a local plugin>";
}
function sourceInstructions(source, state, overrideTarget = "", cleanupTarget = "") {
const target = targetReference(source, state, overrideTarget);
const cleanupPath = sanitizeTarget(cleanupTarget) || target;
if (source === "git") {
return `Clone \`${target}\` to a temporary directory outside the workspace, such as \`/tmp/plugin-validate-$(date +%s)\`. Validate the cloned files there. After the review, run \`rm -rf /tmp/plugin-validate-*\` and verify cleanup with \`ls /tmp/plugin-validate-* 2>&1\`.`;
}
if (source === "zip") {
if (overrideTarget) {
return `The ZIP has already been extracted to \`${target}\`. Validate the plugin from that extracted directory only. Do not install or move it. After the review, delete that extracted directory with \`rm -rf "${cleanupPath}"\` and verify cleanup with \`ls "${cleanupPath}" 2>&1\`.`;
}
return "On run, the selected ZIP will be extracted to a temporary directory for validation. Review the extracted plugin only, do not install it, and delete the extracted directory after the review.";
}
return `Read the plugin directly from \`${target}\`. Do not clone, move, or modify the plugin. No temporary cleanup is required for this source.`;
}
async function parseJsonResponse(response) {
const text = await response.text();
if (!text) return {};
try {
return JSON.parse(text);
} catch {
return { error: text };
}
}
export const store = createStore("pluginValidator", {
source: "local",
localPlugins: [],
localPluginName: "",
gitUrl: "",
zipFile: null,
zipFileName: "",
checks: {},
checksMeta: {},
prompt: "",
output: "",
validating: false,
queued: false,
validationCtxId: "",
get renderedOutput() {
return this.output ? marked.parse(this.output, { breaks: true }) : "";
},
async init() {
const cfg = await loadConfig();
if (!cfg) return;
this.checksMeta = cfg.checks;
const initial = {};
for (const key of Object.keys(cfg.checks)) initial[key] = true;
this.checks = initial;
await this.loadLocalPlugins();
},
async loadLocalPlugins() {
try {
const response = await api.callJsonApi("plugins_list", {
filter: { custom: true, builtin: false, search: "" },
});
const plugins = Array.isArray(response.plugins) ? response.plugins : [];
this.localPlugins = plugins
.filter((plugin) => plugin?.name)
.sort((a, b) => (a.display_name || a.name || "").localeCompare(b.display_name || b.name || ""));
if (!this.localPluginName && this.localPlugins.length) {
const firstPlugin = this.localPlugins[0];
this.localPluginName = firstPlugin && typeof firstPlugin === "object" ? firstPlugin["name"] || "" : "";
}
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
void toastFrontendError(`Failed to load local plugins: ${message}`, "Plugin Validator");
this.localPlugins = [];
this.localPluginName = "";
}
},
applyOptions(options = {}) {
if (options.source) this.source = options.source;
if (typeof options.localPluginName === "string") this.localPluginName = options.localPluginName;
if (typeof options.gitUrl === "string") this.gitUrl = options.gitUrl;
if (options.zipFile) {
this.zipFile = options.zipFile;
this.zipFileName = options.zipFileName || options.zipFile.name || "";
this.source = "zip";
}
},
async onOpen() {
this.output = "";
this.validating = false;
this.queued = false;
this.validationCtxId = "";
await this.loadLocalPlugins();
const cfg = await loadConfig();
if (cfg && Object.keys(this.checks).length === 0) {
this.checksMeta = cfg.checks;
const initial = {};
for (const key of Object.keys(cfg.checks)) initial[key] = true;
this.checks = initial;
}
await this.buildPrompt();
},
cleanup() {
_pollGen++;
},
async openModal(options = {}) {
this.applyOptions(options);
await openAppModal("/plugins/_plugin_validator/webui/plugin-validator.html");
},
async setSource(source) {
this.source = source || "local";
await this.buildPrompt();
},
async selectLocalPlugin(name) {
this.localPluginName = name || "";
await this.buildPrompt();
},
async handleZipUpload(event) {
const file = event?.target?.files?.[0];
if (!file) return;
this.zipFile = file;
this.zipFileName = file.name || "";
await this.buildPrompt();
},
async buildPrompt(targetOverride = "", cleanupTargetOverride = "") {
try {
const [cfg, template, guidance] = await Promise.all([loadConfig(), loadTemplate(), loadGuidance()]);
if (!cfg) return;
const { ratings, checks } = cfg;
const selected = Object.entries(this.checks)
.filter(([, enabled]) => enabled)
.map(([key]) => checks[key])
.filter(Boolean);
let text = template;
text = text.replace(/\{\{SOURCE_LABEL\}\}/g, sourceLabel(this.source));
text = text.replace(/\{\{TARGET_REFERENCE\}\}/g, targetReference(this.source, this, targetOverride));
text = text.replace(/\{\{SOURCE_INSTRUCTIONS\}\}/g, sourceInstructions(this.source, this, targetOverride, cleanupTargetOverride));
text = text.replace(
/\{\{SELECTED_CHECKS\}\}/g,
selected.length ? selected.map((check) => `- ${check.label}`).join("\n") : "- (no validation phases selected)",
);
text = text.replace(
/\{\{CHECK_DETAILS\}\}/g,
selected.length
? selected
.map((check) => `**${check.label}**: ${check.detail}\n${formatCriteria(ratings, check.criteria)}`)
.join("\n\n")
: "(no validation phases selected)",
);
text = text.replace(/\{\{CHECKLIST_GUIDANCE\}\}/g, guidance);
text = text.replace(/\{\{STATUS_LEGEND\}\}/g, formatStatusLegend(ratings));
text = text.replace(/\{\{RATING_ICONS\}\}/g, formatRatingIcons(ratings));
text = text.replace(/\{\{RATING_PASS\}\}/g, ratings.pass.icon);
text = text.replace(/\{\{RATING_WARNING\}\}/g, ratings.warning.icon);
text = text.replace(/\{\{RATING_FAIL\}\}/g, ratings.fail.icon);
this.prompt = text;
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
void toastFrontendError(`Failed to build prompt: ${message}`, "Plugin Validator");
}
},
async copyPrompt() {
try {
await navigator.clipboard.writeText(this.prompt);
} catch {
void toastFrontendError("Failed to copy the validation prompt", "Plugin Validator");
}
},
async _prepareZipForValidation() {
if (!this.zipFile) {
throw new Error("Please select a ZIP file first.");
}
const formData = new FormData();
formData.append("plugin_file", this.zipFile);
const response = await api.fetchApi("/plugins/_plugin_validator/plugin_validator_prepare_zip", {
method: "POST",
body: formData,
});
const data = await parseJsonResponse(response);
if (!response.ok || !data.ok) {
throw new Error(data.error || "ZIP preparation failed.");
}
return data;
},
async runValidation() {
const selectedChecks = Object.entries(this.checks).filter(([, enabled]) => enabled);
if (!selectedChecks.length) {
void toastFrontendError("Select at least one validation phase", "Plugin Validator");
return;
}
let targetOverride = "";
let cleanupTargetOverride = "";
if (this.source === "local") {
if (!this.localPluginName) {
void toastFrontendError("Select a local plugin to validate", "Plugin Validator");
return;
}
} else if (this.source === "git") {
if (!this.gitUrl.trim()) {
void toastFrontendError("Please enter a Git URL", "Plugin Validator");
return;
}
} else if (this.source === "zip") {
try {
const prepared = await this._prepareZipForValidation();
targetOverride = prepared.path || "";
cleanupTargetOverride = prepared.cleanup_path || prepared.path || "";
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
void toastFrontendError(message, "Plugin Validator");
return;
}
}
await this.buildPrompt(targetOverride, cleanupTargetOverride);
const capturedPrompt = this.prompt;
const gen = ++_pollGen;
this.output = "";
let ctxId;
try {
const response = await api.callJsonApi("/chat_create", {});
if (!response.ok) throw new Error("Failed to create chat context");
ctxId = response.ctxid;
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
void toastFrontendError(`Validation failed: ${message}`, "Plugin Validator");
return;
}
this.validationCtxId = ctxId;
if (_running) {
try {
await api.callJsonApi("/plugins/_plugin_validator/plugin_validator_queue", {
context: ctxId,
text: capturedPrompt,
queued: true,
});
} catch {
// Best effort only.
}
_queue.push({ gen, ctxId, prompt: capturedPrompt });
this.queued = true;
this.validating = false;
} else {
try {
await api.callJsonApi("/plugins/_plugin_validator/plugin_validator_queue", {
context: ctxId,
text: capturedPrompt,
});
} catch {
// Best effort only.
}
this.queued = false;
this.validating = true;
this._runNext(gen, ctxId, capturedPrompt);
}
},
async _runNext(gen, ctxId, prompt) {
_running = { gen, ctxId };
try {
await api.callJsonApi("/plugins/_plugin_validator/plugin_validator_start", {
text: prompt,
context: ctxId,
});
await this._pollLoop(gen, ctxId);
} catch (e) {
if (gen === _pollGen) {
const message = e instanceof Error ? e.message : String(e);
void toastFrontendError(`Validation failed: ${message}`, "Plugin Validator");
this.validating = false;
this.queued = false;
}
} finally {
_running = null;
while (_queue.length) {
const next = _queue.shift();
if (!next || next.gen !== _pollGen) {
continue;
}
this.queued = false;
this.validating = true;
this._runNext(next.gen, next.ctxId, next.prompt);
break;
}
}
},
async _pollLoop(gen, ctxId) {
let started = false;
const deadline = Date.now() + MAX_POLL_MS;
while (true) {
if (Date.now() >= deadline) {
if (gen === _pollGen) {
this.validating = false;
void toastFrontendError("Validation timed out while waiting for the agent response", "Plugin Validator");
console.error(`Validation poll timed out for context ${ctxId}`);
}
return;
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
try {
const snapshot = await api.callJsonApi("/poll", {
context: ctxId,
log_from: 0,
notifications_from: 0,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
if (gen === _pollGen && snapshot.logs?.length) {
const last = snapshot.logs
.filter((log) => log.type === "response" && log.no > 0)
.pop();
if (last) this.output = last.content || "";
}
if (snapshot.log_progress_active) started = true;
if (started && !snapshot.log_progress_active) {
if (gen === _pollGen) this.validating = false;
return;
}
if (snapshot.deselect_chat) return;
} catch (e) {
if (gen === _pollGen) {
console.error("Validation poll error:", e);
}
}
}
},
openChatInNewWindow() {
if (!this.validationCtxId) return;
const url = new URL(window.location.href);
url.searchParams.set("ctxid", this.validationCtxId);
window.open(url.toString(), "_blank");
},
});