@enigmax/dashboard 0.1.56 → 0.1.59

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 +150 -1
  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>
@@ -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,130 @@ 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.</div>'
3355
+ + '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><button type="button" class="toggle" data-recall-on="1">Enable recall</button>'
3356
+ + '<span class="set-hint">or run <code>enigma config recall on</code></span></div></div>';
3357
+ $("recallSearch").style.display = "none"; $("recallList").innerHTML = ""; return;
3358
+ }
3359
+ const s = v.stats || {};
3360
+ const types = Object.entries(s.byType || {}).map(([t, n]) => '<span class="tag">' + esc(t) + " " + n + "</span>").join(" ");
3361
+ const last = v.lastSync ? "last sync " + new Date(v.lastSync).toLocaleString() : "not synced yet";
3362
+ $("recallBody").innerHTML = '<div class="panel"><div style="display:flex;justify-content:space-between;align-items:center;gap:10px"><div class="set-cat-h" style="margin:0">Stored memory</div>'
3363
+ + '<button type="button" class="toggle on" data-recall-on="0" title="Turn session memory off">Disable</button></div>'
3364
+ + '<div class="set-hint" style="padding:6px 0">' + (s.observations || 0) + " observations · " + (s.sessions || 0) + " sessions · "
3365
+ + (s.projects || 0) + " projects · " + Math.round((s.dbBytes || 0) / 1024) + " KB · " + esc(last) + "</div>"
3366
+ + (types ? '<div style="padding-top:6px">' + types + "</div>" : "") + "</div>";
3367
+ $("recallSearch").style.display = "";
3368
+ if (!$("recallType").options.length) $("recallType").innerHTML = '<option value="">All types</option>' + RECALL_TYPES.map((t) => '<option value="' + t + '">' + t + "</option>").join("");
3369
+ const sig = (v.projects || []).join("|");
3370
+ if (sig !== recallProjSig) {
3371
+ const keep = $("recallProject").value;
3372
+ $("recallProject").innerHTML = '<option value="">All projects</option>' + (v.projects || []).map((p) => '<option value="' + esc(p) + '">' + esc(p) + "</option>").join("");
3373
+ $("recallProject").value = keep;
3374
+ recallProjSig = sig;
3375
+ }
3376
+ renderRecallList(v.items);
3377
+ }
3378
+ let recallItemsById = {};
3379
+ function recallRowHtml(it, anchorId) {
3380
+ const ic = '<span class="aicon" title="' + esc(it.source) + '" aria-label="' + esc(it.source) + '">' + (agentSvg(it.source) || esc(it.source)) + "</span>";
3381
+ const date = it.createdAt ? new Date(it.createdAt).toISOString().slice(0, 10) : "";
3382
+ const mark = (anchorId && it.id === anchorId) ? ' style="border-left:2px solid var(--accent,#888);padding-left:8px"' : "";
3383
+ return '<div class="set-meta"' + mark + '><div class="set-name">' + ic + ' <span class="tag">' + esc(it.type) + "</span> " + esc(it.title) + "</div>"
3384
+ + '<div class="set-hint" style="opacity:.6">' + esc(it.project) + " · " + date + (it.id ? " · #" + it.id : "") + "</div></div>";
3385
+ }
3386
+ function recallDetailHtml(it) {
3387
+ const tags = (a) => (a && a.length) ? a.map((x) => '<span class="tag">' + esc(x) + "</span>").join(" ") : "";
3388
+ const list = (label, a) => (a && a.length) ? '<div class="set-hint"><strong>' + label + ":</strong> " + esc(a.join(", ")) + "</div>" : "";
3389
+ const facts = (it.facts && it.facts.length) ? "<ul style='margin:4px 0 0 16px'>" + it.facts.map((f) => "<li>" + esc(f) + "</li>").join("") + "</ul>" : "";
3390
+ return '<div style="padding:8px 0 4px 28px">'
3391
+ + (it.narrative ? '<div style="margin-bottom:6px">' + esc(it.narrative) + "</div>" : "")
3392
+ + facts
3393
+ + (it.concepts && it.concepts.length ? '<div style="margin-top:6px">' + tags(it.concepts) + "</div>" : "")
3394
+ + list("Modified", it.files) + list("Read", it.filesRead)
3395
+ + '<div style="margin-top:8px"><button type="button" class="toggle" data-tl="' + it.id + '">Show timeline</button></div>'
3396
+ + '<div class="rtl" id="rtl-' + it.id + '"></div></div>';
3397
+ }
3398
+ function renderRecallList(items) {
3399
+ recallItemsById = {};
3400
+ 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; }
3401
+ $("recallList").innerHTML = '<div class="panel">' + items.map((it) => {
3402
+ if (it.id) recallItemsById[it.id] = it;
3403
+ return '<div class="set-row" style="cursor:pointer" data-rid="' + (it.id || "") + '">' + recallRowHtml(it) + "</div>"
3404
+ + '<div id="rd-' + (it.id || "") + '" style="display:none"></div>';
3405
+ }).join("") + "</div>";
3406
+ }
3407
+ async function loadRecallTimeline(id) {
3408
+ const el = $("rtl-" + id);
3409
+ if (!el) return;
3410
+ el.innerHTML = '<div class="set-hint" style="padding:6px 0">Loading timeline...</div>';
3411
+ try {
3412
+ const out = await (await fetch("/api/recall?timeline=" + id, { cache: "no-store" })).json();
3413
+ const rows = out.items || [];
3414
+ el.innerHTML = rows.length
3415
+ ? '<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>"
3416
+ : '<div class="set-hint">No surrounding context.</div>';
3417
+ } catch { el.innerHTML = '<div class="set-hint err">Could not load timeline.</div>'; }
3418
+ }
3419
+ async function loadRecall() {
3420
+ const q = encodeURIComponent($("recallQ").value || "");
3421
+ const type = encodeURIComponent($("recallType").value || "");
3422
+ const proj = encodeURIComponent($("recallProject").value || "");
3423
+ try { renderRecall(await (await fetch("/api/recall?q=" + q + "&type=" + type + "&project=" + proj, { cache: "no-store" })).json()); }
3424
+ catch { $("recallBody").innerHTML = '<div class="empty" style="padding:12px 0">Recall unavailable.</div>'; }
3425
+ }
3426
+ async function recallAction(op, confirmMsg) {
3427
+ if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, true))) return;
3428
+ setNote("recallNote", op === "sync" ? "Reading transcripts..." : "Working...");
3429
+ try {
3430
+ const out = await (await fetch("/api/recall", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op }) })).json();
3431
+ if (out.view) renderRecall(out.view);
3432
+ setNote("recallNote", out.ok ? (op === "sync" ? "Synced." : "Cleared.") : (out.error || "Action failed."), out.ok ? "ok" : "err");
3433
+ } catch { setNote("recallNote", "Could not reach the server.", "err"); }
3434
+ }
3435
+ async function setRecallEnabled(on) {
3436
+ setNote("recallNote", on ? "Enabling recall..." : "Disabling recall...");
3437
+ try {
3438
+ const out = await (await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "recall", value: on }) })).json();
3439
+ if (!out.ok) { setNote("recallNote", "Could not change recall: " + (out.error || "error"), "err"); return; }
3440
+ setNote("recallNote", out.restartNote || (on ? "Recall enabled." : "Recall disabled."), "ok");
3441
+ loadRecall();
3442
+ } catch { setNote("recallNote", "Could not reach the server.", "err"); }
3443
+ }
3444
+ function wireRecall() {
3445
+ $("recallSync").addEventListener("click", () => recallAction("sync"));
3446
+ $("recallClear").addEventListener("click", () => recallAction("clear", "Delete all stored session memory? This cannot be undone."));
3447
+ $("recallBody").addEventListener("click", (e) => {
3448
+ const t = e.target.closest("[data-recall-on]");
3449
+ if (t) setRecallEnabled(t.dataset.recallOn === "1");
3450
+ });
3451
+ let t; const deb = () => { clearTimeout(t); t = setTimeout(loadRecall, 250); };
3452
+ $("recallQ").addEventListener("input", deb);
3453
+ $("recallType").addEventListener("change", loadRecall);
3454
+ $("recallProject").addEventListener("change", loadRecall);
3455
+ $("recallList").addEventListener("click", (e) => {
3456
+ const tl = e.target.closest("[data-tl]");
3457
+ if (tl) { loadRecallTimeline(tl.dataset.tl); return; }
3458
+ const row = e.target.closest("[data-rid]");
3459
+ if (!row || !row.dataset.rid) return;
3460
+ const detail = $("rd-" + row.dataset.rid);
3461
+ if (!detail) return;
3462
+ if (detail.innerHTML === "") detail.innerHTML = recallDetailHtml(recallItemsById[row.dataset.rid]);
3463
+ detail.style.display = detail.style.display === "none" ? "" : "none";
3464
+ });
3465
+ }
3466
+
3319
3467
  // --- scope (global vs a project) ---------------------------------------------
3320
3468
  // A header selector switches the whole dashboard between Global and a specific
3321
3469
  // project. Project scope is loopback-only and only ever touches a registered
@@ -3607,6 +3755,7 @@ export {
3607
3755
  wireAccounts();
3608
3756
  wireUsage();
3609
3757
  wireResources();
3758
+ wireRecall();
3610
3759
  wireScope();
3611
3760
  $("cfgExport").addEventListener("click", exportConfig);
3612
3761
  $("cfgImport").addEventListener("click", importConfig);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.56",
3
+ "version": "0.1.59",
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": [