@enigmax/dashboard 0.1.31 → 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.
Files changed (2) hide show
  1. package/assets/index.html +72 -8
  2. 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;
@@ -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>
@@ -2157,17 +2169,18 @@ export {
2157
2169
  + '<div class="set-grid">' + c.settings.map(settingRow).join("") + "</div></div>").join("");
2158
2170
  }
2159
2171
 
2160
- async function postSetting(key, value, ctl) {
2161
- $("settingsNote").textContent = "Saving " + key + "...";
2172
+ async function postSetting(key, value, ctl, noteEl) {
2173
+ const note = noteEl || $("settingsNote");
2174
+ note.textContent = "Saving " + key + "...";
2162
2175
  try {
2163
2176
  const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value }) });
2164
2177
  const out = await res.json();
2165
- if (!out.ok) { $("settingsNote").textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
2178
+ if (!out.ok) { note.textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
2166
2179
  const s = out.setting;
2167
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; }
2168
2181
  else if (s) { ctl.value = s.choice; }
2169
- $("settingsNote").textContent = out.restartNote || ("Saved " + key + ".");
2170
- } catch { $("settingsNote").textContent = "Could not reach the server to change " + key + "."; }
2182
+ note.textContent = out.restartNote || ("Saved " + key + ".");
2183
+ } catch { note.textContent = "Could not reach the server to change " + key + "."; }
2171
2184
  }
2172
2185
 
2173
2186
  // List settings: add/remove one entry, then reload so the chips reflect disk truth.
@@ -2389,6 +2402,11 @@ export {
2389
2402
  : s.update === "modified" ? '<span class="tag ext">modified</span>' : "";
2390
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) + '">'
2391
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
+ : "";
2392
2410
  const main = s.source === "enigma"
2393
2411
  ? '<button type="button" class="toggle' + (s.discarded ? "" : " on") + '" data-kind="bool" title="'
2394
2412
  + (s.discarded ? "Disabled - click to enable" : "Enabled - click to disable") + '" data-name="' + esc(s.name)
@@ -2405,10 +2423,12 @@ export {
2405
2423
  + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>").join("") + "</div>"
2406
2424
  : "";
2407
2425
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ext + ver + edited + upd + where + "</div>"
2408
- + '<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>";
2409
2427
  }
2410
2428
 
2429
+ let lastSkills = [];
2411
2430
  function renderSkills(skills) {
2431
+ lastSkills = skills; // kept so the editor can offer per-app save targets
2412
2432
  const enigma = skills.filter((s) => s.source === "enigma");
2413
2433
  const ext = skills.filter((s) => s.source === "external");
2414
2434
  const section = (title, blurb, list, emptyText) => '<div class="set-cat"><div class="set-cat-h">' + title + "</div>"
@@ -2440,8 +2460,9 @@ export {
2440
2460
  if (!out.ok) { $("skillsNote").textContent = "Could not open " + name + ": " + (out.error || "error"); return; }
2441
2461
  editing = name;
2442
2462
  $("seName").textContent = name;
2443
- $("seWarn").textContent = isEnigma ? "managed by enigma - local edits revert on the next update/sync" : "external skill";
2463
+ $("seWarn").textContent = isEnigma ? "managed by enigma - see Customized-skill updates below for whether edits survive an update" : "external skill";
2444
2464
  $("seText").value = out.content || "";
2465
+ editorAgents(lastSkills.find((s) => s.name === name));
2445
2466
  $("skillEditor").style.display = "";
2446
2467
  $("skillsNote").textContent = "";
2447
2468
  window.scrollTo({ top: 0, behavior: "smooth" });
@@ -2449,11 +2470,29 @@ export {
2449
2470
  } catch { $("skillsNote").textContent = "Could not reach the server."; }
2450
2471
  }
2451
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
+ }
2452
2484
  async function saveEditor() {
2453
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
+ }
2454
2493
  $("skillsNote").textContent = "Saving " + editing + "...";
2455
2494
  try {
2456
- const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content: $("seText").value }) });
2495
+ const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content }) });
2457
2496
  const out = await res.json();
2458
2497
  if (!out.ok) { $("skillsNote").textContent = "Could not save " + editing + ": " + (out.error || "error"); return; }
2459
2498
  if (out.skills) renderSkills(out.skills);
@@ -2468,17 +2507,42 @@ export {
2468
2507
  if (!b) return;
2469
2508
  const { name, action } = b.dataset;
2470
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; }
2471
2511
  if (action === "agent-toggle") { postSkill(name, action, { agent: b.dataset.agent, off: b.dataset.off === "1" }); return; }
2472
2512
  if (action === "remove" && !(await askConfirm("Remove skill", 'Remove the external skill "' + name + '" from all agents? This deletes it from disk.', true))) return;
2473
2513
  postSkill(name, action);
2474
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
+ });
2475
2524
  $("seSave").addEventListener("click", saveEditor);
2476
2525
  $("seCancel").addEventListener("click", closeEditor);
2477
2526
  $("skillsCheck").addEventListener("click", () => postSkill("*", "check-updates"));
2478
2527
  }
2479
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
+
2480
2543
  let skillsLoaded = false;
2481
2544
  async function loadSkills() {
2545
+ loadSkillSettings();
2482
2546
  try {
2483
2547
  const res = await fetch("/api/skills", { cache: "no-store" });
2484
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.31",
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": [