@enigmax/dashboard 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/index.html +83 -13
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -214,6 +214,13 @@
|
|
|
214
214
|
border: 1px solid var(--border); border-radius: 8px; padding: 12px;
|
|
215
215
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5;
|
|
216
216
|
}
|
|
217
|
+
/* Per-app save targets in the skill editor. */
|
|
218
|
+
.se-agents { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 10px; }
|
|
219
|
+
.se-agents:empty { display: none; }
|
|
220
|
+
.se-ag-label { font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
221
|
+
.se-ag { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); cursor: pointer; }
|
|
222
|
+
.se-ag input { accent-color: var(--good); }
|
|
223
|
+
.se-ag .aicon { color: var(--muted); }
|
|
217
224
|
.btn-danger {
|
|
218
225
|
background: var(--surface2); color: var(--accent2); border: 1px solid var(--border);
|
|
219
226
|
border-radius: 7px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
|
|
@@ -229,7 +236,7 @@
|
|
|
229
236
|
.achip.on { background: var(--good); color: #06210f; border-color: var(--good); }
|
|
230
237
|
.achip svg { width: 14px; height: 14px; display: block; }
|
|
231
238
|
/* Inline app icons in the "where deployed" tag (static, name in tooltip). */
|
|
232
|
-
.aicons { display: inline-flex; gap: 5px; align-items: center; vertical-align: middle; }
|
|
239
|
+
.aicons { display: inline-flex; gap: 5px; align-items: center; vertical-align: middle; margin-left: 6px; }
|
|
233
240
|
.aicon { display: inline-flex; align-items: center; color: var(--muted); }
|
|
234
241
|
.aicon svg { width: 14px; height: 14px; display: block; }
|
|
235
242
|
.set-ic { vertical-align: middle; margin-right: 7px; }
|
|
@@ -527,11 +534,16 @@
|
|
|
527
534
|
<div id="skillEditor" class="panel" style="display:none">
|
|
528
535
|
<div class="panel-head"><h2>Editing <span id="seName"></span> <small id="seWarn"></small></h2></div>
|
|
529
536
|
<textarea id="seText" class="editor" spellcheck="false"></textarea>
|
|
537
|
+
<div id="seAgents" class="se-agents" data-has="0"></div>
|
|
530
538
|
<div style="margin-top:10px;display:flex;gap:8px">
|
|
531
539
|
<button id="seSave" type="button" class="toggle on">Save</button>
|
|
532
540
|
<button id="seCancel" type="button" class="toggle">Cancel</button>
|
|
533
541
|
</div>
|
|
534
542
|
</div>
|
|
543
|
+
<div class="panel">
|
|
544
|
+
<div class="set-cat-h">Skill settings</div>
|
|
545
|
+
<div id="skillSettings" class="set-grid"></div>
|
|
546
|
+
</div>
|
|
535
547
|
<div class="panel">
|
|
536
548
|
<div id="skillsBody"><div class="empty" style="padding:24px 0">Loading skills...</div></div>
|
|
537
549
|
<div id="skillsNote" class="set-note"></div>
|
|
@@ -2106,11 +2118,17 @@ export {
|
|
|
2106
2118
|
// the label, so the Permissions list reads at a glance. AGENT_ICONS is defined below
|
|
2107
2119
|
// but only referenced at render time, so it is initialised by then.
|
|
2108
2120
|
const AGENT_LABEL_BY_NAME = { claude: "Claude Code", codex: "OpenAI Codex", opencode: "OpenCode" };
|
|
2121
|
+
const GH_ICON = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.52 11.52 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.014 2.898-.014 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>';
|
|
2122
|
+
// Map a setting key to the app/service logo prefixed to its label (per-agent bypass,
|
|
2123
|
+
// the Claude-specific toggles, and the GitHub telemetry toggle).
|
|
2109
2124
|
function settingIcon(key) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2125
|
+
key = key || "";
|
|
2126
|
+
let svg = "", title = "";
|
|
2127
|
+
const bp = /^bypass-(claude|codex|opencode)$/.exec(key);
|
|
2128
|
+
if (bp) { svg = (typeof AGENT_ICONS !== "undefined" && AGENT_ICONS[bp[1]]) || ""; title = AGENT_LABEL_BY_NAME[bp[1]]; }
|
|
2129
|
+
else if (key === "claude-attribution" || key === "claude-survey" || key === "proxy") { svg = (typeof AGENT_ICONS !== "undefined" && AGENT_ICONS.claude) || ""; title = "Claude Code"; }
|
|
2130
|
+
else if (key === "gh-telemetry") { svg = GH_ICON; title = "GitHub"; }
|
|
2131
|
+
return svg ? '<span class="aicon set-ic" title="' + esc(title) + '" aria-hidden="true">' + svg + "</span>" : "";
|
|
2114
2132
|
}
|
|
2115
2133
|
|
|
2116
2134
|
function settingRow(s) {
|
|
@@ -2151,17 +2169,18 @@ export {
|
|
|
2151
2169
|
+ '<div class="set-grid">' + c.settings.map(settingRow).join("") + "</div></div>").join("");
|
|
2152
2170
|
}
|
|
2153
2171
|
|
|
2154
|
-
async function postSetting(key, value, ctl) {
|
|
2155
|
-
|
|
2172
|
+
async function postSetting(key, value, ctl, noteEl) {
|
|
2173
|
+
const note = noteEl || $("settingsNote");
|
|
2174
|
+
note.textContent = "Saving " + key + "...";
|
|
2156
2175
|
try {
|
|
2157
2176
|
const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value }) });
|
|
2158
2177
|
const out = await res.json();
|
|
2159
|
-
if (!out.ok) {
|
|
2178
|
+
if (!out.ok) { note.textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
|
|
2160
2179
|
const s = out.setting;
|
|
2161
2180
|
if (s && ctl.dataset.kind === "bool") { ctl.classList.toggle("on", s.value); ctl.textContent = s.value ? "On" : "Off"; ctl.dataset.on = s.value ? 1 : 0; }
|
|
2162
2181
|
else if (s) { ctl.value = s.choice; }
|
|
2163
|
-
|
|
2164
|
-
} catch {
|
|
2182
|
+
note.textContent = out.restartNote || ("Saved " + key + ".");
|
|
2183
|
+
} catch { note.textContent = "Could not reach the server to change " + key + "."; }
|
|
2165
2184
|
}
|
|
2166
2185
|
|
|
2167
2186
|
// List settings: add/remove one entry, then reload so the chips reflect disk truth.
|
|
@@ -2383,6 +2402,11 @@ export {
|
|
|
2383
2402
|
: s.update === "modified" ? '<span class="tag ext">modified</span>' : "";
|
|
2384
2403
|
const edit = '<button type="button" class="iconbtn" title="Edit" aria-label="Edit" data-name="' + esc(s.name) + '" data-action="edit" data-enigma="' + (s.source === "enigma" ? 1 : 0) + '">'
|
|
2385
2404
|
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></button>';
|
|
2405
|
+
// Propose an improvement: open the skill's source on GitHub (view / edit / open a PR).
|
|
2406
|
+
const propose = s.source === "enigma"
|
|
2407
|
+
? '<button type="button" class="iconbtn" title="Propose an improvement (opens GitHub)" aria-label="Propose an improvement" data-name="' + esc(s.name) + '" data-action="propose">'
|
|
2408
|
+
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg></button>'
|
|
2409
|
+
: "";
|
|
2386
2410
|
const main = s.source === "enigma"
|
|
2387
2411
|
? '<button type="button" class="toggle' + (s.discarded ? "" : " on") + '" data-kind="bool" title="'
|
|
2388
2412
|
+ (s.discarded ? "Disabled - click to enable" : "Enabled - click to disable") + '" data-name="' + esc(s.name)
|
|
@@ -2399,10 +2423,12 @@ export {
|
|
|
2399
2423
|
+ '">' + (agentSvg(a.name) || esc(a.label)) + "</button>").join("") + "</div>"
|
|
2400
2424
|
: "";
|
|
2401
2425
|
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ext + ver + edited + upd + where + "</div>"
|
|
2402
|
-
+ '<div class="set-hint">' + esc(s.description || "") + "</div>" + agentChips + '</div><div class="set-ctl" style="display:flex;gap:8px">' + edit + main + "</div></div>";
|
|
2426
|
+
+ '<div class="set-hint">' + esc(s.description || "") + "</div>" + agentChips + '</div><div class="set-ctl" style="display:flex;gap:8px">' + edit + propose + main + "</div></div>";
|
|
2403
2427
|
}
|
|
2404
2428
|
|
|
2429
|
+
let lastSkills = [];
|
|
2405
2430
|
function renderSkills(skills) {
|
|
2431
|
+
lastSkills = skills; // kept so the editor can offer per-app save targets
|
|
2406
2432
|
const enigma = skills.filter((s) => s.source === "enigma");
|
|
2407
2433
|
const ext = skills.filter((s) => s.source === "external");
|
|
2408
2434
|
const section = (title, blurb, list, emptyText) => '<div class="set-cat"><div class="set-cat-h">' + title + "</div>"
|
|
@@ -2434,8 +2460,9 @@ export {
|
|
|
2434
2460
|
if (!out.ok) { $("skillsNote").textContent = "Could not open " + name + ": " + (out.error || "error"); return; }
|
|
2435
2461
|
editing = name;
|
|
2436
2462
|
$("seName").textContent = name;
|
|
2437
|
-
$("seWarn").textContent = isEnigma ? "managed by enigma -
|
|
2463
|
+
$("seWarn").textContent = isEnigma ? "managed by enigma - see Customized-skill updates below for whether edits survive an update" : "external skill";
|
|
2438
2464
|
$("seText").value = out.content || "";
|
|
2465
|
+
editorAgents(lastSkills.find((s) => s.name === name));
|
|
2439
2466
|
$("skillEditor").style.display = "";
|
|
2440
2467
|
$("skillsNote").textContent = "";
|
|
2441
2468
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
@@ -2443,11 +2470,29 @@ export {
|
|
|
2443
2470
|
} catch { $("skillsNote").textContent = "Could not reach the server."; }
|
|
2444
2471
|
}
|
|
2445
2472
|
function closeEditor() { editing = null; $("skillEditor").style.display = "none"; }
|
|
2473
|
+
// Render per-app save targets (one checkbox per app that holds the skill), only when
|
|
2474
|
+
// the skill is on more than one app - with a single app there is nothing to choose.
|
|
2475
|
+
function editorAgents(skill) {
|
|
2476
|
+
const el = $("seAgents");
|
|
2477
|
+
const states = (skill && skill.agentStates) ? skill.agentStates.filter((a) => a.deployed) : [];
|
|
2478
|
+
if (states.length <= 1) { el.innerHTML = ""; el.dataset.has = "0"; return; }
|
|
2479
|
+
el.dataset.has = "1";
|
|
2480
|
+
el.innerHTML = '<span class="se-ag-label">Save to:</span>' + states.map((a) =>
|
|
2481
|
+
'<label class="se-ag"><input type="checkbox" checked data-agent="' + esc(a.name) + '">'
|
|
2482
|
+
+ '<span class="aicon" aria-hidden="true">' + (agentSvg(a.name) || "") + "</span>" + esc(a.label) + "</label>").join("");
|
|
2483
|
+
}
|
|
2446
2484
|
async function saveEditor() {
|
|
2447
2485
|
if (!editing) return;
|
|
2486
|
+
const agEl = $("seAgents");
|
|
2487
|
+
let content = $("seText").value;
|
|
2488
|
+
if (agEl.dataset.has === "1") {
|
|
2489
|
+
const agents = [...agEl.querySelectorAll("input[type=checkbox]:checked")].map((i) => i.dataset.agent);
|
|
2490
|
+
if (!agents.length) { $("skillsNote").textContent = "Pick at least one app to save to."; return; }
|
|
2491
|
+
content = { text: $("seText").value, agents }; // per-app save
|
|
2492
|
+
}
|
|
2448
2493
|
$("skillsNote").textContent = "Saving " + editing + "...";
|
|
2449
2494
|
try {
|
|
2450
|
-
const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content
|
|
2495
|
+
const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content }) });
|
|
2451
2496
|
const out = await res.json();
|
|
2452
2497
|
if (!out.ok) { $("skillsNote").textContent = "Could not save " + editing + ": " + (out.error || "error"); return; }
|
|
2453
2498
|
if (out.skills) renderSkills(out.skills);
|
|
@@ -2462,17 +2507,42 @@ export {
|
|
|
2462
2507
|
if (!b) return;
|
|
2463
2508
|
const { name, action } = b.dataset;
|
|
2464
2509
|
if (action === "edit") { openEditor(name, b.dataset.enigma === "1"); return; }
|
|
2510
|
+
if (action === "propose") { window.open("https://github.com/FJRG2007/enigma/blob/main/packages/enigma-cli/assets/skills/" + encodeURIComponent(name) + "/SKILL.md", "_blank", "noopener"); return; }
|
|
2465
2511
|
if (action === "agent-toggle") { postSkill(name, action, { agent: b.dataset.agent, off: b.dataset.off === "1" }); return; }
|
|
2466
2512
|
if (action === "remove" && !(await askConfirm("Remove skill", 'Remove the external skill "' + name + '" from all agents? This deletes it from disk.', true))) return;
|
|
2467
2513
|
postSkill(name, action);
|
|
2468
2514
|
});
|
|
2515
|
+
// Skill-related settings live in this tab too; reuse the settings POST, with the note here.
|
|
2516
|
+
$("skillSettings").addEventListener("click", (e) => {
|
|
2517
|
+
const t = e.target.closest("button.toggle[data-skey]");
|
|
2518
|
+
if (t) postSetting(t.dataset.skey, t.dataset.on !== "1", t, $("skillsNote"));
|
|
2519
|
+
});
|
|
2520
|
+
$("skillSettings").addEventListener("change", (e) => {
|
|
2521
|
+
const sel = e.target.closest("select.choice[data-skey]");
|
|
2522
|
+
if (sel) postSetting(sel.dataset.skey, sel.value, sel, $("skillsNote"));
|
|
2523
|
+
});
|
|
2469
2524
|
$("seSave").addEventListener("click", saveEditor);
|
|
2470
2525
|
$("seCancel").addEventListener("click", closeEditor);
|
|
2471
2526
|
$("skillsCheck").addEventListener("click", () => postSkill("*", "check-updates"));
|
|
2472
2527
|
}
|
|
2473
2528
|
|
|
2529
|
+
// The skill-related settings surfaced inside the Skills tab (a better-organised dashboard
|
|
2530
|
+
// than burying them under Settings). They still live in the one settings registry.
|
|
2531
|
+
const SKILL_SETTING_KEYS = ["remote-skills", "skill-update-policy", "auto-sync"];
|
|
2532
|
+
function renderSkillSettings(cats) {
|
|
2533
|
+
const all = (cats || []).flatMap((c) => c.settings || []);
|
|
2534
|
+
const rows = SKILL_SETTING_KEYS.map((k) => all.find((s) => s.key === k)).filter(Boolean);
|
|
2535
|
+
$("skillSettings").innerHTML = rows.length ? rows.map(settingRow).join("")
|
|
2536
|
+
: '<div class="empty" style="padding:8px 0">No skill settings available.</div>';
|
|
2537
|
+
}
|
|
2538
|
+
async function loadSkillSettings() {
|
|
2539
|
+
try { renderSkillSettings((await (await fetch("/api/settings", { cache: "no-store" })).json()).categories || []); }
|
|
2540
|
+
catch { $("skillSettings").innerHTML = ""; }
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2474
2543
|
let skillsLoaded = false;
|
|
2475
2544
|
async function loadSkills() {
|
|
2545
|
+
loadSkillSettings();
|
|
2476
2546
|
try {
|
|
2477
2547
|
const res = await fetch("/api/skills", { cache: "no-store" });
|
|
2478
2548
|
renderSkills((await res.json()).skills || []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|