@enigmax/dashboard 0.1.107 → 0.1.109

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 +84 -24
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -1339,7 +1339,7 @@
1339
1339
  <div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
1340
1340
  <div style="flex:1">
1341
1341
  <h1>Code graph</h1>
1342
- <p>Structural memory of your codebase - a native knowledge graph of the symbols each file defines, the modules it imports and cross-file references. Enabling it exposes enigma's code-graph tools to your agents over MCP. Complements <a href="#/recall">Recall</a> (session memory).</p>
1342
+ <p>Structural memory of your codebase - a native knowledge graph of the symbols each file defines, what imports what, and which symbol calls which. Your agents query it over MCP to find code, trace a change's blast radius and read a file's API surface without opening it. Complements <a href="#/recall">Recall</a> (session memory).</p>
1343
1343
  </div>
1344
1344
  <button id="cgToggle" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Code graph">Off</button>
1345
1345
  <button id="cgRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
@@ -1350,6 +1350,12 @@
1350
1350
  <div class="set-hint" style="padding:4px 0">Build the graph for a codebase. Leave the path empty to index enigma's own working directory.</div>
1351
1351
  <div class="ladd" style="gap:8px;flex-wrap:wrap"><input id="cgRoot" class="linput" placeholder="absolute project path (optional)" style="flex:1;min-width:240px"><button id="cgIndex" type="button" class="toggle on">Index</button></div>
1352
1352
  </div>
1353
+ <div class="panel">
1354
+ <div class="set-cat-h">Ask the graph</div>
1355
+ <div class="set-hint" style="padding:4px 0">Describe a task and get the symbols and files it concerns, ranked, with exact line numbers. The same answer your agents get from the MCP tools.</div>
1356
+ <div class="ladd" style="gap:8px;flex-wrap:wrap"><input id="cgQuery" class="linput" placeholder="where is the session token refreshed?" style="flex:1;min-width:240px"><button id="cgAsk" type="button" class="toggle on">Ask</button></div>
1357
+ <div id="cgAskOut"></div>
1358
+ </div>
1353
1359
  <div id="cgBody"><div class="empty" style="padding:12px 0">Loading...</div></div>
1354
1360
  </section>
1355
1361
 
@@ -3574,7 +3580,8 @@ export {
3574
3580
  + s.choices.map((c) => '<option value="' + esc(c) + '"' + (c === s.choice ? " selected" : "") + ">" + esc(prettyChoice(c)) + "</option>").join("")
3575
3581
  + "</select>"
3576
3582
  : '<button type="button" class="toggle' + (s.value ? " on" : "") + '" data-skey="' + esc(s.key)
3577
- + '" data-kind="bool" data-on="' + (s.value ? 1 : 0) + '">' + (s.value ? "On" : "Off") + "</button>";
3583
+ + '" data-kind="bool" role="switch" aria-checked="' + (s.value ? "true" : "false")
3584
+ + '" data-on="' + (s.value ? 1 : 0) + '">' + (s.value ? "On" : "Off") + "</button>";
3578
3585
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + settingIcon(s.key) + labelWithBadges(s.label) + "</div>"
3579
3586
  + '<div class="set-hint">' + linkify(s.hint) + (s.globalOnly ? " · global" : "") + "</div></div>"
3580
3587
  + '<div class="set-ctl">' + ctl + "</div></div>";
@@ -3592,7 +3599,19 @@ export {
3592
3599
  + '<div class="set-grid">' + c.settings.map(settingRow).join("") + "</div></div>").join("");
3593
3600
  }
3594
3601
 
3595
- function applyBool(ctl, on) { ctl.classList.toggle("on", on); ctl.textContent = on ? "On" : "Off"; ctl.dataset.on = on ? 1 : 0; }
3602
+ // The one painter for every switch on this page: the class the CSS reads, the ARIA state a
3603
+ // screen reader reads, the data-on the click handlers read, and the label when the switch
3604
+ // carries one. Painted before the write lands and rolled back if the server refuses - every
3605
+ // switch here writes through something slow (re-rendering skills, rewriting each agent's
3606
+ // settings and hooks), so one that only moves once the write returns reads as dead.
3607
+ function paintSwitch(btn, on, onLabel, offLabel) {
3608
+ if (!btn) return;
3609
+ btn.classList.toggle("on", !!on);
3610
+ btn.setAttribute("aria-checked", on ? "true" : "false");
3611
+ btn.dataset.on = on ? "1" : "0";
3612
+ if (onLabel != null) btn.textContent = on ? onLabel : offLabel;
3613
+ }
3614
+ function applyBool(ctl, on) { paintSwitch(ctl, on, "On", "Off"); }
3596
3615
 
3597
3616
  // One place that turns an /api/* call into parsed JSON, or an Error naming the real
3598
3617
  // cause. res.json() throws on any reply that is not JSON - an empty 405, or an HTML
@@ -4555,12 +4574,7 @@ export {
4555
4574
  }
4556
4575
  // The two on/off switches at the top of the page. Both are optimistic: the switch flips
4557
4576
  // first and rolls back if the server refuses, like every other control here.
4558
- function gateBool(btn, on) {
4559
- btn.textContent = on ? "on" : "off";
4560
- btn.classList.toggle("on", !!on);
4561
- btn.dataset.on = on ? "1" : "0";
4562
- btn.setAttribute("aria-checked", on ? "true" : "false");
4563
- }
4577
+ function gateBool(btn, on) { paintSwitch(btn, on, "on", "off"); }
4564
4578
  // Defaults for every gate setting, straight from the gate's own config loader (the
4565
4579
  // server resolves an empty config rather than keeping a second copy of the numbers).
4566
4580
  // They drive the "default N" hint and the per-row reset below.
@@ -5567,8 +5581,7 @@ export {
5567
5581
  function renderCodeGraph(view) {
5568
5582
  view = view || {};
5569
5583
  const on = !!view.enabled;
5570
- const t = $("cgToggle");
5571
- t.classList.toggle("on", on); t.setAttribute("aria-checked", on ? "true" : "false"); t.textContent = on ? "On" : "Off";
5584
+ paintSwitch($("cgToggle"), on, "On", "Off");
5572
5585
  const projects = view.projects || [];
5573
5586
  const projHtml = projects.length
5574
5587
  ? projects.map((p) => {
@@ -5580,9 +5593,25 @@ export {
5580
5593
  }).join("")
5581
5594
  : '<div class="empty" style="padding:12px 0">No projects indexed yet. Use "Index a project" above.</div>';
5582
5595
  let html = '<div class="panel"><div class="set-cat-h">Indexed projects <small>' + projects.length + "</small></div>" + projHtml + "</div>";
5596
+ // Drift is stated, not hidden: a graph that has fallen behind the code answers
5597
+ // confidently and wrongly, and the only cue would otherwise be a stale line number.
5598
+ const fresh = view.freshness;
5599
+ if (fresh) {
5600
+ const stale = fresh.stale || 0;
5601
+ html += '<div class="panel"><div class="set-cat-h">Freshness</div><div class="set-row"><div class="set-meta"><div class="set-name">'
5602
+ + (stale === 0 ? "Up to date" : stale + " file" + (stale === 1 ? "" : "s") + " changed since indexing")
5603
+ + '</div><div class="set-hint">' + esc("indexed " + new Date(fresh.indexedAt).toLocaleString()
5604
+ + (fresh.truncated ? " · incomplete: the scan hit a limit, so some files were never indexed" : "")) + "</div></div>"
5605
+ + (stale === 0 ? "" : '<div class="set-ctl"><button type="button" class="toggle on" data-cg-reindex="' + esc(fresh.root) + '">Re-index</button></div>')
5606
+ + "</div></div>";
5607
+ }
5583
5608
  if (view.architecture) html += cgSection("Architecture" + (view.selected ? " - " + view.selected : ""), view.architecture);
5584
5609
  if (view.schema) html += cgSection("Graph schema", view.schema);
5585
5610
  $("cgBody").innerHTML = html;
5611
+ const ask = view.ask;
5612
+ $("cgAskOut").innerHTML = ask
5613
+ ? '<pre style="white-space:pre-wrap;overflow-x:auto;margin:10px 0 0;font-size:12px">' + esc(ask.report) + "</pre>"
5614
+ : "";
5586
5615
  }
5587
5616
  async function loadCodeGraph() {
5588
5617
  $("cgNote").textContent = "";
@@ -5600,20 +5629,44 @@ export {
5600
5629
  } catch { setNote("cgNote", "Could not reach the server.", "err"); return null; }
5601
5630
  }
5602
5631
  async function cgToggleAction() {
5603
- const turnOn = $("cgToggle").getAttribute("aria-checked") !== "true";
5604
- const out = await cgPost({ op: "toggle", on: turnOn, project: cgProject || undefined }, "Saving...");
5605
- if (out) setNote("cgNote", out.ok ? (turnOn ? "Code graph enabled - tools exposed to your agents (restart them to load it)." : "Code graph disabled.") : (out.error || "Action failed."), out.ok ? "ok" : "err");
5632
+ const btn = $("cgToggle");
5633
+ const was = btn.getAttribute("aria-checked") === "true";
5634
+ paintSwitch(btn, !was, "On", "Off");
5635
+ const out = await cgPost({ op: "toggle", on: !was }, was ? "Disabling..." : "Enabling...");
5636
+ if (!out || !out.ok) {
5637
+ paintSwitch(btn, was, "On", "Off");
5638
+ if (out) setNote("cgNote", out.error || "Action failed.", "err"); // cgPost already reported an unreachable server
5639
+ return;
5640
+ }
5641
+ paintSwitch(btn, !!out.enabled, "On", "Off"); // the server's answer wins over the optimistic paint
5642
+ setNote("cgNote", out.enabled
5643
+ ? "Code graph enabled - tools and session hooks wired; agents pick it up on their next session."
5644
+ : "Code graph disabled - tools and session hooks removed.", "ok");
5606
5645
  }
5607
5646
  async function cgIndexAction() {
5608
5647
  const root = $("cgRoot").value.trim();
5609
5648
  const out = await cgPost({ op: "index", root: root || undefined }, "Indexing...");
5610
5649
  if (out) { if (out.view && out.view.selected) cgProject = out.view.selected; setNote("cgNote", out.ok ? "Indexed." : (out.error || "Indexing failed."), out.ok ? "ok" : "err"); }
5611
5650
  }
5651
+ async function cgAskAction() {
5652
+ const query = $("cgQuery").value.trim();
5653
+ if (!query) { setNote("cgNote", "Type a question first.", "err"); return; }
5654
+ const out = await cgPost({ op: "ask", query, project: cgProject || undefined }, "Searching...");
5655
+ if (out) setNote("cgNote", out.ok ? "" : (out.error || "Query failed."), out.ok ? "ok" : "err");
5656
+ }
5612
5657
  function wireCodeGraph() {
5613
5658
  $("cgRefresh").addEventListener("click", loadCodeGraph);
5614
5659
  $("cgToggle").addEventListener("click", cgToggleAction);
5615
5660
  $("cgIndex").addEventListener("click", cgIndexAction);
5661
+ $("cgAsk").addEventListener("click", cgAskAction);
5662
+ $("cgQuery").addEventListener("keydown", (e) => { if (e.key === "Enter") cgAskAction(); });
5616
5663
  $("view-codegraph").addEventListener("click", (e) => {
5664
+ const reindex = e.target.closest("button[data-cg-reindex]");
5665
+ if (reindex) {
5666
+ cgPost({ op: "index", root: reindex.dataset.cgReindex }, "Re-indexing...")
5667
+ .then((out) => { if (out) setNote("cgNote", out.ok ? "Re-indexed." : (out.error || "Indexing failed."), out.ok ? "ok" : "err"); });
5668
+ return;
5669
+ }
5617
5670
  const b = e.target.closest("button[data-cg-project]");
5618
5671
  if (!b) return;
5619
5672
  cgProject = b.dataset.cgProject;
@@ -5730,7 +5783,7 @@ export {
5730
5783
  $("pgSend").addEventListener("click", pgSend);
5731
5784
  $("pgSaveDefault").addEventListener("click", pgSaveDefault);
5732
5785
  const tog = $("pgTools");
5733
- tog.addEventListener("click", () => { const on = tog.getAttribute("aria-checked") !== "true"; tog.classList.toggle("on", on); tog.setAttribute("aria-checked", on ? "true" : "false"); tog.textContent = on ? "On" : "Off"; });
5786
+ tog.addEventListener("click", () => paintSwitch(tog, tog.getAttribute("aria-checked") !== "true", "On", "Off"));
5734
5787
  $("pgMode").addEventListener("change", pgSyncMode);
5735
5788
  const copyBtn = $("pgCopyCurl");
5736
5789
  // The button is an icon, so the toast is not the only confirmation: swap the icon for a
@@ -5823,11 +5876,7 @@ export {
5823
5876
  $("recallKey").disabled = !!p.keyFromEnv;
5824
5877
  // claude-local needs no base/key; hide that row for it.
5825
5878
  $("recallKeyRow").style.display = p.provider === "claude-local" ? "none" : "";
5826
- const llmBtn = $("recallLlmBtn");
5827
- llmBtn.textContent = p.llm ? "on" : "off";
5828
- llmBtn.classList.toggle("on", !!p.llm);
5829
- llmBtn.dataset.on = p.llm ? "1" : "0";
5830
- llmBtn.setAttribute("aria-checked", p.llm ? "true" : "false");
5879
+ paintSwitch($("recallLlmBtn"), p.llm, "on", "off");
5831
5880
  }
5832
5881
  let recallItemsById = {};
5833
5882
  // Multi-select state for the Recall list: a Set of selected ids, the ordered ids as
@@ -5922,14 +5971,25 @@ export {
5922
5971
  setNote("recallNote", out.ok ? (op === "sync" ? "Synced." : "Cleared.") : (out.error || "Action failed."), out.ok ? "ok" : "err");
5923
5972
  } catch { setNote("recallNote", "Could not reach the server.", "err"); }
5924
5973
  }
5925
- async function setRecallEnabled(on) {
5974
+ // The recall switch carries its state in its own label ("Enable recall" when off, "Disable"
5975
+ // when on), in its title and in what the next click asks for - so an optimistic paint has to
5976
+ // move all three. Painting the switch alone left a button reading "Enable recall" while
5977
+ // already on, offering to turn on what it had just turned on, until loadRecall() re-rendered.
5978
+ function paintRecallSwitch(btn, on) {
5979
+ paintSwitch(btn, on, "Disable", "Enable recall");
5980
+ if (!btn) return;
5981
+ btn.dataset.recallOn = on ? "0" : "1";
5982
+ btn.title = on ? "Turn session memory off" : "Turn session memory on";
5983
+ }
5984
+ async function setRecallEnabled(on, btn) {
5985
+ paintRecallSwitch(btn, on);
5926
5986
  setNote("recallNote", on ? "Enabling recall..." : "Disabling recall...");
5927
5987
  try {
5928
5988
  const out = await (await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "recall", value: on }) })).json();
5929
- if (!out.ok) { setNote("recallNote", "Could not change recall: " + (out.error || "error"), "err"); return; }
5989
+ if (!out.ok) { paintRecallSwitch(btn, !on); setNote("recallNote", "Could not change recall: " + (out.error || "error"), "err"); return; }
5930
5990
  setNote("recallNote", out.restartNote || (on ? "Recall enabled." : "Recall disabled."), out.restartNote ? "warn" : "ok");
5931
5991
  loadRecall();
5932
- } catch { setNote("recallNote", "Could not reach the server.", "err"); }
5992
+ } catch { paintRecallSwitch(btn, !on); setNote("recallNote", "Could not reach the server.", "err"); }
5933
5993
  }
5934
5994
  async function recallProviderPost(extra, msg) {
5935
5995
  setNote("recallNote", msg || "Saving provider...");
@@ -5954,7 +6014,7 @@ export {
5954
6014
  $("recallClear").addEventListener("click", () => recallAction("clear", "Delete all stored session memory? This cannot be undone."));
5955
6015
  $("recallBody").addEventListener("click", (e) => {
5956
6016
  const t = e.target.closest("[data-recall-on]");
5957
- if (t) setRecallEnabled(t.dataset.recallOn === "1");
6017
+ if (t) setRecallEnabled(t.dataset.recallOn === "1", t);
5958
6018
  });
5959
6019
  // Provider panel: save provider; flip LLM curation. The provider picker is the
5960
6020
  // custom icon dropdown (recallProvVal), wired in renderRecallProvider.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.107",
3
+ "version": "0.1.109",
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": [