@enigmax/dashboard 0.1.52 → 0.1.54

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 +73 -15
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -361,6 +361,13 @@
361
361
  }
362
362
  .modal-back.show .modal { transform: none; opacity: 1; }
363
363
  .modal.wide { max-width: 900px; }
364
+ .as-top { display: flex; gap: 8px; margin: 14px 0 6px; }
365
+ .as-list { max-height: 50vh; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 4px 12px; }
366
+ .as-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--surface2); cursor: pointer; }
367
+ .as-row:last-child { border-bottom: none; }
368
+ .as-row input { accent-color: var(--accent); width: 15px; height: 15px; flex-shrink: 0; }
369
+ .as-name { font-size: 13px; color: var(--text); font-family: var(--mono); }
370
+ .as-src { margin-left: auto; display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
364
371
  .modal-title { font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--text); }
365
372
  .modal-msg { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 8px; word-break: break-word; }
366
373
  .modal-input {
@@ -632,6 +639,11 @@
632
639
  <div class="set-cat-h">Skill settings</div>
633
640
  <div id="skillSettings" class="set-grid"></div>
634
641
  </div>
642
+ <div class="panel" id="memPanel">
643
+ <div class="set-cat-h">Agent memory</div>
644
+ <div class="set-cat-b">Customize the instruction files enigma deploys to each agent (CLAUDE.md / AGENTS.md). Your edits follow the Customized-skill updates policy on the next sync: overwrite restores the managed version, keep preserves them.</div>
645
+ <div id="memBody" class="set-grid"><div class="empty" style="padding:12px 0">Loading...</div></div>
646
+ </div>
635
647
  <div class="panel">
636
648
  <div id="skillsBody"><div class="empty" style="padding:24px 0">Loading skills...</div></div>
637
649
  <div id="skillsNote" class="set-note"></div>
@@ -664,11 +676,6 @@
664
676
  </div>
665
677
  </div>
666
678
  </div>
667
- <div class="panel" id="memPanel">
668
- <div class="set-cat-h">Agent memory</div>
669
- <div class="set-cat-b">Customize the instruction files enigma deploys to each agent (CLAUDE.md / AGENTS.md). Your edits follow the Customized-skill updates policy on the next sync: overwrite restores the managed version, keep preserves them.</div>
670
- <div id="memBody" class="set-grid"><div class="empty" style="padding:12px 0">Loading...</div></div>
671
- </div>
672
679
  </section>
673
680
 
674
681
  <section id="view-resources" style="display:none">
@@ -2596,12 +2603,10 @@ export {
2596
2603
  async function loadSettings() {
2597
2604
  if (isProjectScope()) return loadProjectSettings();
2598
2605
  const port = $("dashPortPanel"); if (port) port.style.display = ""; // global-only panel
2599
- const mp = $("memPanel"); if (mp) mp.style.display = "";
2600
2606
  try {
2601
2607
  const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
2602
2608
  renderSettings(data.categories || []);
2603
2609
  renderDashPort(data);
2604
- loadMemory();
2605
2610
  } catch { $("settingsBody").innerHTML = '<div class="empty" style="padding:24px 0">Settings unavailable.</div>'; }
2606
2611
  }
2607
2612
 
@@ -2650,7 +2655,7 @@ export {
2650
2655
  }
2651
2656
 
2652
2657
  async function loadMemory(project) {
2653
- const el = project ? $("projMemBody") : $("memBody");
2658
+ const el = $("memBody"); // global + project share this panel (one scope shown at a time)
2654
2659
  if (!el) return;
2655
2660
  try {
2656
2661
  const q = project ? "/api/memory?path=" + encodeURIComponent(project) : "/api/memory";
@@ -2703,8 +2708,8 @@ export {
2703
2708
  } catch { toast("Could not reach the server.", "err"); }
2704
2709
  }
2705
2710
 
2706
- // Edit/reset buttons live in both the global panel (#memBody) and the project view
2707
- // (#projMemBody, mutually exclusive by scope); one document-level delegate covers both.
2711
+ // The memory editor lives in the Skills tab (#memBody, scope-aware via memProject());
2712
+ // a document-level delegate handles its edit/reset buttons in global and project scope.
2708
2713
  document.addEventListener("click", (e) => {
2709
2714
  const ed = e.target.closest && e.target.closest("[data-mem-edit]");
2710
2715
  if (ed) { openMemoryEditor(ed.dataset.memEdit, memProject()); return; }
@@ -2952,7 +2957,7 @@ export {
2952
2957
  $("skillsBody").innerHTML =
2953
2958
  '<div class="panel"><div class="set-cat-h">Auto-detect skills by stack</div>'
2954
2959
  + '<div class="set-cat-b">Scan this project (packages, config files, gems) and install the matching community skills for its stack - hash-verified and kept separate from the policy skills below.</div>'
2955
- + '<div class="ladd"><button type="button" class="toggle on with-icon" data-proj-autoskills><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>Detect &amp; install</button></div></div>'
2960
+ + '<div class="ladd"><button type="button" class="toggle on with-icon" data-proj-autoskills><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>Detect skills</button></div></div>'
2956
2961
  + section("Policy skills", "Deploy enigma's policy skills into this project, per app. A green chip means the skill is in that app's project folder.", skills, "No skills.");
2957
2962
  return;
2958
2963
  }
@@ -3069,6 +3074,7 @@ export {
3069
3074
  if (isProjectScope()) return loadProjectSkills();
3070
3075
  skillScope = "global";
3071
3076
  loadSkillSettings();
3077
+ loadMemory();
3072
3078
  try {
3073
3079
  const res = await fetch("/api/skills", { cache: "no-store" });
3074
3080
  renderSkills((await res.json()).skills || []);
@@ -3434,7 +3440,6 @@ export {
3434
3440
  async function loadProjectSettings() {
3435
3441
  const el = $("settingsBody");
3436
3442
  const port = $("dashPortPanel"); if (port) port.style.display = "none"; // global-only
3437
- const mp = $("memPanel"); if (mp) mp.style.display = "none"; // global panel; project memory is rendered inline below
3438
3443
  el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
3439
3444
  let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
3440
3445
  if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
@@ -3449,13 +3454,11 @@ export {
3449
3454
  + '<div class="set-row"><div class="set-meta"><div class="set-name">Git hooks</div><div class="set-hint">commit secret guard for this repo</div></div><div class="set-ctl">' + hooks + "</div></div>"
3450
3455
  + '<div class="set-row"><div class="set-meta"><div class="set-name">AI quality gate</div><div class="set-hint">validate changes through the gate pipeline before push (needs the gate feature enabled)</div></div><div class="set-ctl">' + gate + "</div></div></div></div>"
3451
3456
  + '<div class="panel"><div class="set-cat-h">Project config</div><div class="set-cat-b">applies only to this project (its .enigma.json). Settings not shown here are global-only - switch Scope to Global for those.</div><div class="set-grid">' + cfg + "</div></div>"
3452
- + '<div class="panel"><div class="set-cat-h">Agent memory</div><div class="set-cat-b">Customize this project\'s instruction files (CLAUDE.md / AGENTS.md). Edits follow the Customized-skill updates policy on the next sync.</div><div id="projMemBody" class="set-grid"><div class="empty" style="padding:12px 0">Loading...</div></div></div>'
3453
3457
  + '<div class="panel"><div class="set-cat-h">Manage</div><div class="set-grid">'
3454
3458
  + '<div class="set-row"><div class="set-meta"><div class="set-name">Name</div><div class="set-hint">display name for this project (must be unique)</div></div><div class="set-ctl"><input class="linput" data-proj-name value="' + esc(d.label) + '" spellcheck="false" style="width:200px"></div></div>'
3455
3459
  + '<div class="set-row"><div class="set-meta"><div class="set-name">Description</div><div class="set-hint">optional note shown in the header</div></div><div class="set-ctl ladd" style="align-items:center"><input class="linput" data-proj-desc value="' + esc(d.description || "") + '" placeholder="optional" style="flex:none;width:240px"><button type="button" class="toggle on" data-proj-save-meta>Save</button></div></div>'
3456
3460
  + '<div class="set-row"><div class="set-meta"><div class="set-name">Forget project</div><div class="set-hint">remove it from the dashboard list - its files are not touched</div></div><div class="set-ctl"><button type="button" class="toggle" data-proj-forget>Forget</button></div></div>'
3457
3461
  + "</div></div>";
3458
- loadMemory(currentScope);
3459
3462
  }
3460
3463
  async function saveProjectMeta() {
3461
3464
  if (!isProjectScope()) return;
@@ -3474,6 +3477,7 @@ export {
3474
3477
  skillScope = "project";
3475
3478
  const el = $("skillsBody");
3476
3479
  const ss = $("skillSettings"); if (ss) ss.innerHTML = '<div class="set-cat-b" style="padding:0 0 4px">Skill settings are global - switch Scope to Global to change them.</div>';
3480
+ loadMemory(currentScope);
3477
3481
  el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
3478
3482
  let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
3479
3483
  if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
@@ -3498,6 +3502,60 @@ export {
3498
3502
  loadProjectsList(); // refresh selector badges
3499
3503
  } catch { setNote(activeView === "skills" ? "skillsNote" : "settingsNote", "Could not reach the server.", "err"); }
3500
3504
  }
3505
+ // Autoskills: detect the project's stack, let the user pick which community skills to
3506
+ // install (all checked by default), then install the selected ones - or cancel.
3507
+ async function autoskillsFlow() {
3508
+ if (!isProjectScope()) return;
3509
+ setNote("skillsNote", "Detecting your stack...");
3510
+ let out;
3511
+ try { out = await (await fetch("/api/projects/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentScope, op: "autoskills-detect" }) })).json(); }
3512
+ catch { setNote("skillsNote", "Could not reach the server.", "err"); return; }
3513
+ if (!out.ok) { setNote("skillsNote", out.error || "Detection failed.", "err"); return; }
3514
+ const skills = out.skills || [];
3515
+ if (!skills.length) { setNote("skillsNote", (out.detected ? "Detected " + out.detected + " - " : "") + "no matching community skills.", "ok"); return; }
3516
+ setNote("skillsNote", out.detected ? ("Detected " + out.detected + ".") : "");
3517
+ const selected = await openAutoskillsModal(out.detected, skills);
3518
+ if (selected == null) { setNote("skillsNote", "Cancelled."); return; }
3519
+ if (!selected.length) { setNote("skillsNote", "Nothing selected.", "err"); return; }
3520
+ setNote("skillsNote", "Installing " + selected.length + " skill(s)...");
3521
+ try {
3522
+ const ins = await (await fetch("/api/projects/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentScope, op: "autoskills-install", skills: selected }) })).json();
3523
+ if (ins.ok) { setNote("skillsNote", ins.note || "Installed.", "ok"); loadProjectSkills(); loadProjectsList(); }
3524
+ else setNote("skillsNote", ins.error || ins.note || "Install failed.", "err");
3525
+ } catch { setNote("skillsNote", "Could not reach the server.", "err"); }
3526
+ }
3527
+
3528
+ // Selection modal: one checkbox per detected skill (all checked), Select all / Clear, and
3529
+ // Install selected / Cancel. Resolves to the chosen refs, or null when cancelled.
3530
+ function openAutoskillsModal(detected, skills) {
3531
+ return new Promise((resolve) => {
3532
+ const back = document.createElement("div");
3533
+ back.className = "modal-back";
3534
+ const rows = skills.map((s, i) =>
3535
+ '<label class="as-row"><input type="checkbox" checked data-as="' + i + '">'
3536
+ + '<span class="as-name">' + esc(s.name) + "</span>"
3537
+ + '<span class="as-src">' + (s.sources || []).map((x) => '<span class="tag">' + esc(x) + "</span>").join("") + "</span></label>").join("");
3538
+ back.innerHTML = '<div class="modal wide" role="dialog" aria-modal="true">'
3539
+ + '<div class="modal-title">Install stack skills</div>'
3540
+ + '<div class="modal-msg">' + (detected ? "Detected " + esc(detected) + ". " : "") + esc(skills.length + " matching community skill(s)") + " - pick which to install. They are hash-verified and kept separate from the policy skills.</div>"
3541
+ + '<div class="as-top"><button type="button" class="toggle" data-as-all>Select all</button><button type="button" class="toggle" data-as-none>Clear</button></div>'
3542
+ + '<div class="as-list">' + rows + "</div>"
3543
+ + '<div class="modal-acts"><button type="button" class="toggle modal-cancel">Cancel</button>'
3544
+ + '<button type="button" class="toggle on modal-ok">Install selected</button></div></div>';
3545
+ document.body.appendChild(back);
3546
+ requestAnimationFrame(() => back.classList.add("show"));
3547
+ let done = false;
3548
+ const close = (val) => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); resolve(val); };
3549
+ const boxes = () => Array.from(back.querySelectorAll("input[data-as]"));
3550
+ back.querySelector("[data-as-all]").addEventListener("click", () => boxes().forEach((b) => { b.checked = true; }));
3551
+ back.querySelector("[data-as-none]").addEventListener("click", () => boxes().forEach((b) => { b.checked = false; }));
3552
+ back.querySelector(".modal-cancel").addEventListener("click", () => close(null));
3553
+ back.addEventListener("mousedown", (e) => { if (e.target === back) close(null); });
3554
+ back.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); close(null); } });
3555
+ back.querySelector(".modal-ok").addEventListener("click", () => close(boxes().filter((b) => b.checked).map((b) => skills[Number(b.dataset.as)].ref)));
3556
+ });
3557
+ }
3558
+
3501
3559
  async function forgetProject() {
3502
3560
  if (!isProjectScope()) return;
3503
3561
  if (!(await askConfirm("Forget project", "Stop managing " + scopeLabel() + "? Its files are not touched.", false))) return;
@@ -3509,7 +3567,7 @@ export {
3509
3567
  }
3510
3568
  function onProjectControlClick(e) {
3511
3569
  if (!isProjectScope()) return;
3512
- if (e.target.closest("[data-proj-autoskills]")) { projAction({ op: "autoskills" }, "Scan this project and install the matching community skills for its stack?"); return; }
3570
+ if (e.target.closest("[data-proj-autoskills]")) { autoskillsFlow(); return; }
3513
3571
  const chip = e.target.closest("[data-pskill]"); // per-app deploy chip in the Skills view
3514
3572
  if (chip) { projAction({ op: "skill", name: chip.dataset.pskill, agent: chip.dataset.pagent, on: chip.dataset.poff !== "1" }); return; }
3515
3573
  const sk = e.target.closest("[data-proj-skill]");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
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": [