@enigmax/dashboard 0.1.49 → 0.1.52

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 +110 -4
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -156,6 +156,9 @@
156
156
  .set-meta { flex: 1; min-width: 0; }
157
157
  .set-name { font-size: 13px; color: var(--text); }
158
158
  .set-hint { font-size: 12px; color: var(--muted); margin-top: 2px; }
159
+ /* Capitalize the first letter of every option description/blurb in the dashboard
160
+ (presentation only; the shared registry keeps its lowercase TUI/CLI convention). */
161
+ .set-hint::first-letter, .set-cat-b::first-letter, .lnone::first-letter { text-transform: uppercase; }
159
162
  .set-ctl { flex-shrink: 0; }
160
163
  .toggle {
161
164
  background: var(--surface2); color: var(--muted); border: 1px solid var(--border);
@@ -247,6 +250,7 @@
247
250
  .achip { display: inline-flex; align-items: center; justify-content: center; padding: 5px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); cursor: pointer; text-decoration: none; transition: color .15s, background .15s, border-color .15s; }
248
251
  .achip:hover { border-color: #2f3744; color: var(--text); }
249
252
  .achip.on { background: var(--good); color: #06210f; border-color: var(--good); }
253
+ .achip:disabled { opacity: .35; cursor: not-allowed; }
250
254
  .achip svg { width: 14px; height: 14px; display: block; }
251
255
  /* Inline app icons in the "where deployed" tag (static, name in tooltip). */
252
256
  .aicons { display: inline-flex; gap: 5px; align-items: center; vertical-align: middle; margin-left: 6px; }
@@ -356,6 +360,7 @@
356
360
  transform: translateY(10px) scale(.98); opacity: 0; transition: transform .18s ease, opacity .18s ease;
357
361
  }
358
362
  .modal-back.show .modal { transform: none; opacity: 1; }
363
+ .modal.wide { max-width: 900px; }
359
364
  .modal-title { font-family: var(--mono); font-size: 14px; font-weight: 600; color: var(--text); }
360
365
  .modal-msg { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 8px; word-break: break-word; }
361
366
  .modal-input {
@@ -659,6 +664,11 @@
659
664
  </div>
660
665
  </div>
661
666
  </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>
662
672
  </section>
663
673
 
664
674
  <section id="view-resources" style="display:none">
@@ -2586,10 +2596,12 @@ export {
2586
2596
  async function loadSettings() {
2587
2597
  if (isProjectScope()) return loadProjectSettings();
2588
2598
  const port = $("dashPortPanel"); if (port) port.style.display = ""; // global-only panel
2599
+ const mp = $("memPanel"); if (mp) mp.style.display = "";
2589
2600
  try {
2590
2601
  const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
2591
2602
  renderSettings(data.categories || []);
2592
2603
  renderDashPort(data);
2604
+ loadMemory();
2593
2605
  } catch { $("settingsBody").innerHTML = '<div class="empty" style="padding:24px 0">Settings unavailable.</div>'; }
2594
2606
  }
2595
2607
 
@@ -2613,6 +2625,93 @@ export {
2613
2625
  } catch { setNote("settingsNote", "Could not reach the server to set the port.", "err"); }
2614
2626
  }
2615
2627
 
2628
+ // --- agent memory editor (CLAUDE.md / AGENTS.md), global or per project ----------
2629
+ // Global edits each agent's home memory file; project scope edits <project>/CLAUDE.md
2630
+ // and <project>/AGENTS.md. Writes go through /api/memory, which marks the edit so the
2631
+ // overwrite/keep policy (Customized-skill updates) can preserve it on the next sync.
2632
+ function memProject() { return isProjectScope() ? currentScope : undefined; }
2633
+
2634
+ async function postMemory(id, action, content, project) {
2635
+ const body = { id, action };
2636
+ if (content !== undefined) body.content = content;
2637
+ if (project) body.path = project;
2638
+ return (await fetch("/api/memory", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })).json();
2639
+ }
2640
+
2641
+ function memoryRow(g) {
2642
+ const apps = (g.agents || []).map((a) => '<span class="tag">' + esc(a.label) + "</span>").join(" ");
2643
+ const state = !g.deployed ? '<span class="tag">not deployed</span>'
2644
+ : g.edited ? '<span class="tag exp">edited</span>'
2645
+ : g.managed ? '<span class="tag meas">managed</span>' : '<span class="tag">custom</span>';
2646
+ const reset = (g.deployed && g.edited) ? ' <button type="button" class="iconbtn" data-mem-reset="' + esc(g.id) + '" title="Restore the managed version">reset</button>' : "";
2647
+ return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(g.file) + " " + apps + " " + state + "</div>"
2648
+ + '<div class="set-hint">' + (g.deployed ? "Deployed - edit to customize" : "Not deployed yet - editing creates it") + "</div></div>"
2649
+ + '<div class="set-ctl ladd" style="align-items:center"><button type="button" class="toggle on" data-mem-edit="' + esc(g.id) + '">Edit</button>' + reset + "</div></div>";
2650
+ }
2651
+
2652
+ async function loadMemory(project) {
2653
+ const el = project ? $("projMemBody") : $("memBody");
2654
+ if (!el) return;
2655
+ try {
2656
+ const q = project ? "/api/memory?path=" + encodeURIComponent(project) : "/api/memory";
2657
+ const data = await (await fetch(q, { cache: "no-store" })).json();
2658
+ const groups = data.groups || [];
2659
+ el.innerHTML = groups.length ? groups.map(memoryRow).join("")
2660
+ : '<div class="empty" style="padding:12px 0">No agents detected to deploy memory to.</div>';
2661
+ } catch { el.innerHTML = '<div class="empty" style="padding:12px 0">Memory unavailable.</div>'; }
2662
+ }
2663
+
2664
+ async function openMemoryEditor(id, project) {
2665
+ let out;
2666
+ try { out = await postMemory(id, "read", undefined, project); }
2667
+ catch { toast("Could not reach the server.", "err"); return; }
2668
+ if (!out.ok) { toast(out.error || "Could not read the memory file.", "err"); return; }
2669
+ const back = document.createElement("div");
2670
+ back.className = "modal-back";
2671
+ back.innerHTML = '<div class="modal wide" role="dialog" aria-modal="true">'
2672
+ + '<div class="modal-title">Editing ' + esc(id) + " memory</div>"
2673
+ + '<div class="modal-msg">Saved to every agent that reads this file. On the next sync the Customized-skill updates policy decides whether your edit is kept or replaced by the managed version.</div>'
2674
+ + '<textarea class="editor" spellcheck="false" style="margin-top:14px"></textarea>'
2675
+ + '<div class="modal-acts"><button type="button" class="toggle modal-cancel">Cancel</button>'
2676
+ + '<button type="button" class="toggle on modal-ok">Save</button></div></div>';
2677
+ document.body.appendChild(back);
2678
+ requestAnimationFrame(() => back.classList.add("show"));
2679
+ const ta = back.querySelector("textarea");
2680
+ ta.value = out.content || "";
2681
+ let done = false;
2682
+ const close = () => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); };
2683
+ back.querySelector(".modal-cancel").addEventListener("click", close);
2684
+ back.addEventListener("mousedown", (e) => { if (e.target === back) close(); });
2685
+ back.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); close(); } });
2686
+ back.querySelector(".modal-ok").addEventListener("click", async () => {
2687
+ const btn = back.querySelector(".modal-ok"); btn.disabled = true;
2688
+ try {
2689
+ const r = await postMemory(id, "save", ta.value, project);
2690
+ if (r.ok) { toast(r.note || "Saved.", "ok"); close(); loadMemory(project); }
2691
+ else { toast(r.error || "Could not save.", "err"); btn.disabled = false; }
2692
+ } catch { toast("Could not reach the server.", "err"); btn.disabled = false; }
2693
+ });
2694
+ ta.focus();
2695
+ }
2696
+
2697
+ async function resetMemory(id, project) {
2698
+ if (!(await askConfirm("Restore managed memory?", "This replaces your edits with enigma's managed version.", true))) return;
2699
+ try {
2700
+ const r = await postMemory(id, "reset", undefined, project);
2701
+ if (r.ok) { toast(r.note || "Restored.", "ok"); loadMemory(project); }
2702
+ else toast(r.error || "Could not restore.", "err");
2703
+ } catch { toast("Could not reach the server.", "err"); }
2704
+ }
2705
+
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.
2708
+ document.addEventListener("click", (e) => {
2709
+ const ed = e.target.closest && e.target.closest("[data-mem-edit]");
2710
+ if (ed) { openMemoryEditor(ed.dataset.memEdit, memProject()); return; }
2711
+ const rs = e.target.closest && e.target.closest("[data-mem-reset]");
2712
+ if (rs) { resetMemory(rs.dataset.memReset, memProject()); }
2713
+ });
2714
+
2616
2715
  // --- "Enigma systems" overview (factual state, never invented savings) ---
2617
2716
  function pill(state, text) { return '<span class="pill ' + state + '">' + esc(text) + "</span>"; }
2618
2717
  function boolPill(b) { return b ? pill("on", "on") : pill("off", "off"); }
@@ -2787,9 +2886,13 @@ export {
2787
2886
  // Project scope: same row, but the per-app chips toggle deployment INTO this project
2788
2887
  // (data-pskill, handled by the scope delegate) and the global-only controls are hidden.
2789
2888
  if (skillScope === "project") {
2790
- const chips = (s.agentStates || []).map((a) =>
2791
- '<button type="button" class="achip' + (a.deployed ? " on" : "") + '" data-pskill="' + esc(s.name) + '" data-pagent="' + esc(a.name) + '" data-poff="' + (a.deployed ? 1 : 0) + '" aria-label="' + esc(a.label) + '" title="'
2792
- + (a.deployed ? esc(a.label) + " - deployed, click to remove" : esc(a.label) + " - not deployed, click to deploy") + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>").join("");
2889
+ const chips = (s.agentStates || []).map((a) => {
2890
+ // An app that is not installed on this machine can't be a deploy target - show
2891
+ // its chip disabled rather than letting a click create a dir for an unused agent.
2892
+ if (a.installed === false) return '<button type="button" class="achip" disabled aria-label="' + esc(a.label) + '" title="' + esc(a.label) + ' - not installed">' + (agentSvg(a.name) || esc(a.label)) + "</button>";
2893
+ return '<button type="button" class="achip' + (a.deployed ? " on" : "") + '" data-pskill="' + esc(s.name) + '" data-pagent="' + esc(a.name) + '" data-poff="' + (a.deployed ? 1 : 0) + '" aria-label="' + esc(a.label) + '" title="'
2894
+ + (a.deployed ? esc(a.label) + " - deployed, click to remove" : esc(a.label) + " - not deployed, click to deploy") + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>";
2895
+ }).join("");
2793
2896
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ver + "</div>"
2794
2897
  + '<div class="set-hint">' + esc(s.description || "") + "</div>"
2795
2898
  + '<div class="set-agents">' + (chips || '<span class="tag">no agents</span>') + "</div></div></div>";
@@ -3331,6 +3434,7 @@ export {
3331
3434
  async function loadProjectSettings() {
3332
3435
  const el = $("settingsBody");
3333
3436
  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
3334
3438
  el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
3335
3439
  let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
3336
3440
  if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
@@ -3345,11 +3449,13 @@ export {
3345
3449
  + '<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>"
3346
3450
  + '<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>"
3347
3451
  + '<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>'
3348
3453
  + '<div class="panel"><div class="set-cat-h">Manage</div><div class="set-grid">'
3349
3454
  + '<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>'
3350
3455
  + '<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>'
3351
3456
  + '<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>'
3352
3457
  + "</div></div>";
3458
+ loadMemory(currentScope);
3353
3459
  }
3354
3460
  async function saveProjectMeta() {
3355
3461
  if (!isProjectScope()) return;
@@ -3376,7 +3482,7 @@ export {
3376
3482
  const agents = d.agents || [];
3377
3483
  const skills = (d.available || []).map((s) => ({
3378
3484
  name: s.name, description: s.description, source: "enigma",
3379
- agentStates: agents.map((a) => ({ name: a.name, label: a.label, deployed: (a.deployed || []).indexOf(s.name) !== -1 })),
3485
+ agentStates: agents.map((a) => ({ name: a.name, label: a.label, installed: a.installed, deployed: (a.deployed || []).indexOf(s.name) !== -1 })),
3380
3486
  }));
3381
3487
  renderSkills(skills);
3382
3488
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.49",
3
+ "version": "0.1.52",
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": [