@enigmax/dashboard 0.1.55 → 0.1.58

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 +145 -13
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -426,6 +426,7 @@
426
426
  <a href="#/skills" data-view="skills" class="tab">Skills</a>
427
427
  <a href="#/settings" data-view="settings" class="tab">Settings</a>
428
428
  <a href="#/resources" data-view="resources" class="tab">Resources</a>
429
+ <a href="#/recall" data-view="recall" class="tab">Recall</a>
429
430
  </nav>
430
431
  <div class="nav">
431
432
  <label class="scope-wrap" title="Scope: global config or a specific project"><span class="scope-lbl">Scope</span><select id="scopeSel" class="scope-sel"><option value="global">Global</option></select></label>
@@ -696,6 +697,28 @@
696
697
  <div class="panel"><div class="set-cat-h">Processes <small>by memory</small></div><div id="resProcs"><div class="empty" style="padding:12px 0">Loading...</div></div></div>
697
698
  </section>
698
699
 
700
+ <section id="view-recall" style="display:none">
701
+ <div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
702
+ <div style="flex:1">
703
+ <h1>Recall</h1>
704
+ <p>Searchable memory of your past coding sessions, built locally from your own transcripts. Agents can query it over MCP (<code>enigma_recall</code>); it never leaves this machine.</p>
705
+ </div>
706
+ <button id="recallSync" type="button" class="toggle on" title="Read new transcripts into the store">Sync now</button>
707
+ <button id="recallClear" type="button" class="btn-danger" title="Delete all stored memory">Clear</button>
708
+ </div>
709
+ <div id="recallNote" class="set-note"></div>
710
+ <div id="recallBody"><div class="empty" style="padding:12px 0">Loading...</div></div>
711
+ <div id="recallSearch" class="panel" style="display:none">
712
+ <div class="set-cat-h">Search</div>
713
+ <div class="ladd" style="gap:8px;flex-wrap:wrap">
714
+ <input id="recallQ" class="linput" type="search" placeholder="Search your session memory..." style="flex:1;min-width:220px">
715
+ <select id="recallType" class="linput" style="max-width:160px"></select>
716
+ <select id="recallProject" class="linput" style="max-width:200px"></select>
717
+ </div>
718
+ </div>
719
+ <div id="recallList"></div>
720
+ </section>
721
+
699
722
  <footer>
700
723
  Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
701
724
  Data source: <code>~/.enigma/ccr/stats.json</code> + <code>history.jsonl</code> - reset it with <code>enigma compress --clear</code>. Enigma <span id="ver"></span>
@@ -2892,16 +2915,16 @@ export {
2892
2915
  let skillScope = "global"; // "global" | "project" - the same rows render in both
2893
2916
  function skillRow(s) {
2894
2917
  const ver = s.version ? '<span class="tag">v' + esc(s.version) + "</span>" : "";
2895
- // Project scope: same row, but the per-app chips toggle deployment INTO this project
2896
- // (data-pskill, handled by the scope delegate) and the global-only controls are hidden.
2918
+ // Project scope: same row, but policy skills are managed globally - the per-app chips are
2919
+ // read-only here (show presence only) and the global-only controls are hidden.
2897
2920
  if (skillScope === "project") {
2898
- const chips = (s.agentStates || []).map((a) => {
2899
- // An app that is not installed on this machine can't be a deploy target - show
2900
- // its chip disabled rather than letting a click create a dir for an unused agent.
2901
- 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>";
2902
- 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="'
2903
- + (a.deployed ? esc(a.label) + " - deployed, click to remove" : esc(a.label) + " - not deployed, click to deploy") + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>";
2904
- }).join("");
2921
+ // Policy (enigma) skills are managed at GLOBAL scope - deploying one per-project is
2922
+ // redundant (an agent reads both global and project skills). So in project scope the
2923
+ // per-app chips are READ-ONLY: they show where the skill is present but never toggle
2924
+ // (no data-pskill). Switch Scope to Global to enable/disable a policy skill.
2925
+ const chips = (s.agentStates || []).map((a) =>
2926
+ '<button type="button" class="achip' + (a.deployed ? " on" : "") + '" disabled aria-label="' + esc(a.label) + '" title="'
2927
+ + esc(a.label) + (a.installed === false ? " - not installed" : " - policy skills are managed at Global scope") + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>").join("");
2905
2928
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ver + "</div>"
2906
2929
  + '<div class="set-hint">' + esc(s.description || "") + "</div>"
2907
2930
  + '<div class="set-agents">' + (chips || '<span class="tag">no agents</span>') + "</div></div></div>";
@@ -2962,7 +2985,7 @@ export {
2962
2985
  '<div class="panel"><div class="set-cat-h">Auto-detect skills by stack</div>'
2963
2986
  + '<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>'
2964
2987
  + '<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>'
2965
- + 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.");
2988
+ + section("Policy skills", "enigma's policy skills are managed at Global scope (an agent reads them in every project). Shown here read-only - a green app logo means the skill is also present in this project's folder. Switch Scope to Global to enable or disable a policy skill.", skills, "No skills.");
2966
2989
  return;
2967
2990
  }
2968
2991
  const enigma = skills.filter((s) => s.source === "enigma");
@@ -3237,7 +3260,7 @@ export {
3237
3260
 
3238
3261
  // --- hash routing between the subpages ---
3239
3262
  // Single source of truth for the subpage ids (savings is the default/fallback).
3240
- const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources"];
3263
+ const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources", "recall"];
3241
3264
  // Views that apply to a single project; the rest are global/machine-level only.
3242
3265
  const PROJECT_VIEWS = ["settings", "skills"];
3243
3266
  let activeView = "savings";
@@ -3257,6 +3280,7 @@ export {
3257
3280
  else if (v === "accounts") { if (!accountsLoaded) { accountsLoaded = true; loadAccounts(); } }
3258
3281
  else if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
3259
3282
  else if (v === "resources") loadResources();
3283
+ else if (v === "recall") loadRecall();
3260
3284
  // The chart was sized while its view may have been hidden; nudge it on return.
3261
3285
  else if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
3262
3286
  }
@@ -3316,6 +3340,115 @@ export {
3316
3340
  });
3317
3341
  }
3318
3342
 
3343
+ // --- recall subpage (local session memory; reads your own transcripts) ---
3344
+ const RECALL_TYPES = ["bugfix", "feature", "refactor", "change", "discovery", "decision", "security"];
3345
+ let recallProjSig = "";
3346
+ function renderRecall(v) {
3347
+ v = v || {};
3348
+ if (!v.available) {
3349
+ $("recallBody").innerHTML = '<div class="panel"><div class="empty" style="padding:16px 0">Recall needs the enigma binary - open the dashboard via the enigma app.</div></div>';
3350
+ $("recallSearch").style.display = "none"; $("recallList").innerHTML = ""; return;
3351
+ }
3352
+ if (!v.enabled) {
3353
+ $("recallBody").innerHTML = '<div class="panel"><div class="set-cat-h">Session memory is off</div>'
3354
+ + '<div class="set-hint" style="padding:8px 0">Build a searchable memory of your coding sessions from your own transcripts. Turn it on with:</div>'
3355
+ + '<code>enigma config recall on</code></div>';
3356
+ $("recallSearch").style.display = "none"; $("recallList").innerHTML = ""; return;
3357
+ }
3358
+ const s = v.stats || {};
3359
+ const types = Object.entries(s.byType || {}).map(([t, n]) => '<span class="tag">' + esc(t) + " " + n + "</span>").join(" ");
3360
+ const last = v.lastSync ? "last sync " + new Date(v.lastSync).toLocaleString() : "not synced yet";
3361
+ $("recallBody").innerHTML = '<div class="panel"><div class="set-cat-h">Stored memory</div>'
3362
+ + '<div class="set-hint" style="padding:6px 0">' + (s.observations || 0) + " observations · " + (s.sessions || 0) + " sessions · "
3363
+ + (s.projects || 0) + " projects · " + Math.round((s.dbBytes || 0) / 1024) + " KB · " + esc(last) + "</div>"
3364
+ + (types ? '<div style="padding-top:6px">' + types + "</div>" : "") + "</div>";
3365
+ $("recallSearch").style.display = "";
3366
+ if (!$("recallType").options.length) $("recallType").innerHTML = '<option value="">All types</option>' + RECALL_TYPES.map((t) => '<option value="' + t + '">' + t + "</option>").join("");
3367
+ const sig = (v.projects || []).join("|");
3368
+ if (sig !== recallProjSig) {
3369
+ const keep = $("recallProject").value;
3370
+ $("recallProject").innerHTML = '<option value="">All projects</option>' + (v.projects || []).map((p) => '<option value="' + esc(p) + '">' + esc(p) + "</option>").join("");
3371
+ $("recallProject").value = keep;
3372
+ recallProjSig = sig;
3373
+ }
3374
+ renderRecallList(v.items);
3375
+ }
3376
+ let recallItemsById = {};
3377
+ function recallRowHtml(it, anchorId) {
3378
+ const ic = '<span class="aicon" title="' + esc(it.source) + '" aria-label="' + esc(it.source) + '">' + (agentSvg(it.source) || esc(it.source)) + "</span>";
3379
+ const date = it.createdAt ? new Date(it.createdAt).toISOString().slice(0, 10) : "";
3380
+ const mark = (anchorId && it.id === anchorId) ? ' style="border-left:2px solid var(--accent,#888);padding-left:8px"' : "";
3381
+ return '<div class="set-meta"' + mark + '><div class="set-name">' + ic + ' <span class="tag">' + esc(it.type) + "</span> " + esc(it.title) + "</div>"
3382
+ + '<div class="set-hint" style="opacity:.6">' + esc(it.project) + " · " + date + (it.id ? " · #" + it.id : "") + "</div></div>";
3383
+ }
3384
+ function recallDetailHtml(it) {
3385
+ const tags = (a) => (a && a.length) ? a.map((x) => '<span class="tag">' + esc(x) + "</span>").join(" ") : "";
3386
+ const list = (label, a) => (a && a.length) ? '<div class="set-hint"><strong>' + label + ":</strong> " + esc(a.join(", ")) + "</div>" : "";
3387
+ const facts = (it.facts && it.facts.length) ? "<ul style='margin:4px 0 0 16px'>" + it.facts.map((f) => "<li>" + esc(f) + "</li>").join("") + "</ul>" : "";
3388
+ return '<div style="padding:8px 0 4px 28px">'
3389
+ + (it.narrative ? '<div style="margin-bottom:6px">' + esc(it.narrative) + "</div>" : "")
3390
+ + facts
3391
+ + (it.concepts && it.concepts.length ? '<div style="margin-top:6px">' + tags(it.concepts) + "</div>" : "")
3392
+ + list("Modified", it.files) + list("Read", it.filesRead)
3393
+ + '<div style="margin-top:8px"><button type="button" class="toggle" data-tl="' + it.id + '">Show timeline</button></div>'
3394
+ + '<div class="rtl" id="rtl-' + it.id + '"></div></div>';
3395
+ }
3396
+ function renderRecallList(items) {
3397
+ recallItemsById = {};
3398
+ if (!items || !items.length) { $("recallList").innerHTML = '<div class="empty" style="padding:16px 0">No memories yet - run a sync, or adjust your search.</div>'; return; }
3399
+ $("recallList").innerHTML = '<div class="panel">' + items.map((it) => {
3400
+ if (it.id) recallItemsById[it.id] = it;
3401
+ return '<div class="set-row" style="cursor:pointer" data-rid="' + (it.id || "") + '">' + recallRowHtml(it) + "</div>"
3402
+ + '<div id="rd-' + (it.id || "") + '" style="display:none"></div>';
3403
+ }).join("") + "</div>";
3404
+ }
3405
+ async function loadRecallTimeline(id) {
3406
+ const el = $("rtl-" + id);
3407
+ if (!el) return;
3408
+ el.innerHTML = '<div class="set-hint" style="padding:6px 0">Loading timeline...</div>';
3409
+ try {
3410
+ const out = await (await fetch("/api/recall?timeline=" + id, { cache: "no-store" })).json();
3411
+ const rows = out.items || [];
3412
+ el.innerHTML = rows.length
3413
+ ? '<div style="margin-top:6px;border-top:1px solid rgba(128,128,128,.2)">' + rows.map((r) => '<div class="set-row">' + recallRowHtml(r, Number(id)) + "</div>").join("") + "</div>"
3414
+ : '<div class="set-hint">No surrounding context.</div>';
3415
+ } catch { el.innerHTML = '<div class="set-hint err">Could not load timeline.</div>'; }
3416
+ }
3417
+ async function loadRecall() {
3418
+ const q = encodeURIComponent($("recallQ").value || "");
3419
+ const type = encodeURIComponent($("recallType").value || "");
3420
+ const proj = encodeURIComponent($("recallProject").value || "");
3421
+ try { renderRecall(await (await fetch("/api/recall?q=" + q + "&type=" + type + "&project=" + proj, { cache: "no-store" })).json()); }
3422
+ catch { $("recallBody").innerHTML = '<div class="empty" style="padding:12px 0">Recall unavailable.</div>'; }
3423
+ }
3424
+ async function recallAction(op, confirmMsg) {
3425
+ if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, true))) return;
3426
+ setNote("recallNote", op === "sync" ? "Reading transcripts..." : "Working...");
3427
+ try {
3428
+ const out = await (await fetch("/api/recall", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op }) })).json();
3429
+ if (out.view) renderRecall(out.view);
3430
+ setNote("recallNote", out.ok ? (op === "sync" ? "Synced." : "Cleared.") : (out.error || "Action failed."), out.ok ? "ok" : "err");
3431
+ } catch { setNote("recallNote", "Could not reach the server.", "err"); }
3432
+ }
3433
+ function wireRecall() {
3434
+ $("recallSync").addEventListener("click", () => recallAction("sync"));
3435
+ $("recallClear").addEventListener("click", () => recallAction("clear", "Delete all stored session memory? This cannot be undone."));
3436
+ let t; const deb = () => { clearTimeout(t); t = setTimeout(loadRecall, 250); };
3437
+ $("recallQ").addEventListener("input", deb);
3438
+ $("recallType").addEventListener("change", loadRecall);
3439
+ $("recallProject").addEventListener("change", loadRecall);
3440
+ $("recallList").addEventListener("click", (e) => {
3441
+ const tl = e.target.closest("[data-tl]");
3442
+ if (tl) { loadRecallTimeline(tl.dataset.tl); return; }
3443
+ const row = e.target.closest("[data-rid]");
3444
+ if (!row || !row.dataset.rid) return;
3445
+ const detail = $("rd-" + row.dataset.rid);
3446
+ if (!detail) return;
3447
+ if (detail.innerHTML === "") detail.innerHTML = recallDetailHtml(recallItemsById[row.dataset.rid]);
3448
+ detail.style.display = detail.style.display === "none" ? "" : "none";
3449
+ });
3450
+ }
3451
+
3319
3452
  // --- scope (global vs a project) ---------------------------------------------
3320
3453
  // A header selector switches the whole dashboard between Global and a specific
3321
3454
  // project. Project scope is loopback-only and only ever touches a registered
@@ -3572,8 +3705,6 @@ export {
3572
3705
  function onProjectControlClick(e) {
3573
3706
  if (!isProjectScope()) return;
3574
3707
  if (e.target.closest("[data-proj-autoskills]")) { autoskillsFlow(); return; }
3575
- const chip = e.target.closest("[data-pskill]"); // per-app deploy chip in the Skills view
3576
- if (chip) { projAction({ op: "skill", name: chip.dataset.pskill, agent: chip.dataset.pagent, on: chip.dataset.poff !== "1" }); return; }
3577
3708
  const sk = e.target.closest("[data-proj-skill]");
3578
3709
  if (sk) { projAction({ op: "skill", name: sk.dataset.projSkill, on: sk.dataset.on !== "1" }); return; }
3579
3710
  const cb = e.target.closest("[data-proj-cfg-bool]");
@@ -3609,6 +3740,7 @@ export {
3609
3740
  wireAccounts();
3610
3741
  wireUsage();
3611
3742
  wireResources();
3743
+ wireRecall();
3612
3744
  wireScope();
3613
3745
  $("cfgExport").addEventListener("click", exportConfig);
3614
3746
  $("cfgImport").addEventListener("click", importConfig);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.55",
3
+ "version": "0.1.58",
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": [