@enigmax/dashboard 0.1.30 → 0.1.32

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 +83 -13
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -214,6 +214,13 @@
214
214
  border: 1px solid var(--border); border-radius: 8px; padding: 12px;
215
215
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5;
216
216
  }
217
+ /* Per-app save targets in the skill editor. */
218
+ .se-agents { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 10px; }
219
+ .se-agents:empty { display: none; }
220
+ .se-ag-label { font-family: var(--mono); font-size: 12px; color: var(--muted); }
221
+ .se-ag { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); cursor: pointer; }
222
+ .se-ag input { accent-color: var(--good); }
223
+ .se-ag .aicon { color: var(--muted); }
217
224
  .btn-danger {
218
225
  background: var(--surface2); color: var(--accent2); border: 1px solid var(--border);
219
226
  border-radius: 7px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
@@ -229,7 +236,7 @@
229
236
  .achip.on { background: var(--good); color: #06210f; border-color: var(--good); }
230
237
  .achip svg { width: 14px; height: 14px; display: block; }
231
238
  /* Inline app icons in the "where deployed" tag (static, name in tooltip). */
232
- .aicons { display: inline-flex; gap: 5px; align-items: center; vertical-align: middle; }
239
+ .aicons { display: inline-flex; gap: 5px; align-items: center; vertical-align: middle; margin-left: 6px; }
233
240
  .aicon { display: inline-flex; align-items: center; color: var(--muted); }
234
241
  .aicon svg { width: 14px; height: 14px; display: block; }
235
242
  .set-ic { vertical-align: middle; margin-right: 7px; }
@@ -527,11 +534,16 @@
527
534
  <div id="skillEditor" class="panel" style="display:none">
528
535
  <div class="panel-head"><h2>Editing <span id="seName"></span> <small id="seWarn"></small></h2></div>
529
536
  <textarea id="seText" class="editor" spellcheck="false"></textarea>
537
+ <div id="seAgents" class="se-agents" data-has="0"></div>
530
538
  <div style="margin-top:10px;display:flex;gap:8px">
531
539
  <button id="seSave" type="button" class="toggle on">Save</button>
532
540
  <button id="seCancel" type="button" class="toggle">Cancel</button>
533
541
  </div>
534
542
  </div>
543
+ <div class="panel">
544
+ <div class="set-cat-h">Skill settings</div>
545
+ <div id="skillSettings" class="set-grid"></div>
546
+ </div>
535
547
  <div class="panel">
536
548
  <div id="skillsBody"><div class="empty" style="padding:24px 0">Loading skills...</div></div>
537
549
  <div id="skillsNote" class="set-note"></div>
@@ -2106,11 +2118,17 @@ export {
2106
2118
  // the label, so the Permissions list reads at a glance. AGENT_ICONS is defined below
2107
2119
  // but only referenced at render time, so it is initialised by then.
2108
2120
  const AGENT_LABEL_BY_NAME = { claude: "Claude Code", codex: "OpenAI Codex", opencode: "OpenCode" };
2121
+ const GH_ICON = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.52 11.52 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.014 2.898-.014 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>';
2122
+ // Map a setting key to the app/service logo prefixed to its label (per-agent bypass,
2123
+ // the Claude-specific toggles, and the GitHub telemetry toggle).
2109
2124
  function settingIcon(key) {
2110
- const m = /^bypass-(claude|codex|opencode)$/.exec(key || "");
2111
- if (!m) return "";
2112
- const ic = (typeof AGENT_ICONS !== "undefined" && AGENT_ICONS[m[1]]) || "";
2113
- return ic ? '<span class="aicon set-ic" title="' + esc(AGENT_LABEL_BY_NAME[m[1]]) + '" aria-hidden="true">' + ic + "</span>" : "";
2125
+ key = key || "";
2126
+ let svg = "", title = "";
2127
+ const bp = /^bypass-(claude|codex|opencode)$/.exec(key);
2128
+ if (bp) { svg = (typeof AGENT_ICONS !== "undefined" && AGENT_ICONS[bp[1]]) || ""; title = AGENT_LABEL_BY_NAME[bp[1]]; }
2129
+ else if (key === "claude-attribution" || key === "claude-survey" || key === "proxy") { svg = (typeof AGENT_ICONS !== "undefined" && AGENT_ICONS.claude) || ""; title = "Claude Code"; }
2130
+ else if (key === "gh-telemetry") { svg = GH_ICON; title = "GitHub"; }
2131
+ return svg ? '<span class="aicon set-ic" title="' + esc(title) + '" aria-hidden="true">' + svg + "</span>" : "";
2114
2132
  }
2115
2133
 
2116
2134
  function settingRow(s) {
@@ -2151,17 +2169,18 @@ export {
2151
2169
  + '<div class="set-grid">' + c.settings.map(settingRow).join("") + "</div></div>").join("");
2152
2170
  }
2153
2171
 
2154
- async function postSetting(key, value, ctl) {
2155
- $("settingsNote").textContent = "Saving " + key + "...";
2172
+ async function postSetting(key, value, ctl, noteEl) {
2173
+ const note = noteEl || $("settingsNote");
2174
+ note.textContent = "Saving " + key + "...";
2156
2175
  try {
2157
2176
  const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value }) });
2158
2177
  const out = await res.json();
2159
- if (!out.ok) { $("settingsNote").textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
2178
+ if (!out.ok) { note.textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
2160
2179
  const s = out.setting;
2161
2180
  if (s && ctl.dataset.kind === "bool") { ctl.classList.toggle("on", s.value); ctl.textContent = s.value ? "On" : "Off"; ctl.dataset.on = s.value ? 1 : 0; }
2162
2181
  else if (s) { ctl.value = s.choice; }
2163
- $("settingsNote").textContent = out.restartNote || ("Saved " + key + ".");
2164
- } catch { $("settingsNote").textContent = "Could not reach the server to change " + key + "."; }
2182
+ note.textContent = out.restartNote || ("Saved " + key + ".");
2183
+ } catch { note.textContent = "Could not reach the server to change " + key + "."; }
2165
2184
  }
2166
2185
 
2167
2186
  // List settings: add/remove one entry, then reload so the chips reflect disk truth.
@@ -2383,6 +2402,11 @@ export {
2383
2402
  : s.update === "modified" ? '<span class="tag ext">modified</span>' : "";
2384
2403
  const edit = '<button type="button" class="iconbtn" title="Edit" aria-label="Edit" data-name="' + esc(s.name) + '" data-action="edit" data-enigma="' + (s.source === "enigma" ? 1 : 0) + '">'
2385
2404
  + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></button>';
2405
+ // Propose an improvement: open the skill's source on GitHub (view / edit / open a PR).
2406
+ const propose = s.source === "enigma"
2407
+ ? '<button type="button" class="iconbtn" title="Propose an improvement (opens GitHub)" aria-label="Propose an improvement" data-name="' + esc(s.name) + '" data-action="propose">'
2408
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg></button>'
2409
+ : "";
2386
2410
  const main = s.source === "enigma"
2387
2411
  ? '<button type="button" class="toggle' + (s.discarded ? "" : " on") + '" data-kind="bool" title="'
2388
2412
  + (s.discarded ? "Disabled - click to enable" : "Enabled - click to disable") + '" data-name="' + esc(s.name)
@@ -2399,10 +2423,12 @@ export {
2399
2423
  + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>").join("") + "</div>"
2400
2424
  : "";
2401
2425
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ext + ver + edited + upd + where + "</div>"
2402
- + '<div class="set-hint">' + esc(s.description || "") + "</div>" + agentChips + '</div><div class="set-ctl" style="display:flex;gap:8px">' + edit + main + "</div></div>";
2426
+ + '<div class="set-hint">' + esc(s.description || "") + "</div>" + agentChips + '</div><div class="set-ctl" style="display:flex;gap:8px">' + edit + propose + main + "</div></div>";
2403
2427
  }
2404
2428
 
2429
+ let lastSkills = [];
2405
2430
  function renderSkills(skills) {
2431
+ lastSkills = skills; // kept so the editor can offer per-app save targets
2406
2432
  const enigma = skills.filter((s) => s.source === "enigma");
2407
2433
  const ext = skills.filter((s) => s.source === "external");
2408
2434
  const section = (title, blurb, list, emptyText) => '<div class="set-cat"><div class="set-cat-h">' + title + "</div>"
@@ -2434,8 +2460,9 @@ export {
2434
2460
  if (!out.ok) { $("skillsNote").textContent = "Could not open " + name + ": " + (out.error || "error"); return; }
2435
2461
  editing = name;
2436
2462
  $("seName").textContent = name;
2437
- $("seWarn").textContent = isEnigma ? "managed by enigma - local edits revert on the next update/sync" : "external skill";
2463
+ $("seWarn").textContent = isEnigma ? "managed by enigma - see Customized-skill updates below for whether edits survive an update" : "external skill";
2438
2464
  $("seText").value = out.content || "";
2465
+ editorAgents(lastSkills.find((s) => s.name === name));
2439
2466
  $("skillEditor").style.display = "";
2440
2467
  $("skillsNote").textContent = "";
2441
2468
  window.scrollTo({ top: 0, behavior: "smooth" });
@@ -2443,11 +2470,29 @@ export {
2443
2470
  } catch { $("skillsNote").textContent = "Could not reach the server."; }
2444
2471
  }
2445
2472
  function closeEditor() { editing = null; $("skillEditor").style.display = "none"; }
2473
+ // Render per-app save targets (one checkbox per app that holds the skill), only when
2474
+ // the skill is on more than one app - with a single app there is nothing to choose.
2475
+ function editorAgents(skill) {
2476
+ const el = $("seAgents");
2477
+ const states = (skill && skill.agentStates) ? skill.agentStates.filter((a) => a.deployed) : [];
2478
+ if (states.length <= 1) { el.innerHTML = ""; el.dataset.has = "0"; return; }
2479
+ el.dataset.has = "1";
2480
+ el.innerHTML = '<span class="se-ag-label">Save to:</span>' + states.map((a) =>
2481
+ '<label class="se-ag"><input type="checkbox" checked data-agent="' + esc(a.name) + '">'
2482
+ + '<span class="aicon" aria-hidden="true">' + (agentSvg(a.name) || "") + "</span>" + esc(a.label) + "</label>").join("");
2483
+ }
2446
2484
  async function saveEditor() {
2447
2485
  if (!editing) return;
2486
+ const agEl = $("seAgents");
2487
+ let content = $("seText").value;
2488
+ if (agEl.dataset.has === "1") {
2489
+ const agents = [...agEl.querySelectorAll("input[type=checkbox]:checked")].map((i) => i.dataset.agent);
2490
+ if (!agents.length) { $("skillsNote").textContent = "Pick at least one app to save to."; return; }
2491
+ content = { text: $("seText").value, agents }; // per-app save
2492
+ }
2448
2493
  $("skillsNote").textContent = "Saving " + editing + "...";
2449
2494
  try {
2450
- const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content: $("seText").value }) });
2495
+ const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editing, action: "save", content }) });
2451
2496
  const out = await res.json();
2452
2497
  if (!out.ok) { $("skillsNote").textContent = "Could not save " + editing + ": " + (out.error || "error"); return; }
2453
2498
  if (out.skills) renderSkills(out.skills);
@@ -2462,17 +2507,42 @@ export {
2462
2507
  if (!b) return;
2463
2508
  const { name, action } = b.dataset;
2464
2509
  if (action === "edit") { openEditor(name, b.dataset.enigma === "1"); return; }
2510
+ if (action === "propose") { window.open("https://github.com/FJRG2007/enigma/blob/main/packages/enigma-cli/assets/skills/" + encodeURIComponent(name) + "/SKILL.md", "_blank", "noopener"); return; }
2465
2511
  if (action === "agent-toggle") { postSkill(name, action, { agent: b.dataset.agent, off: b.dataset.off === "1" }); return; }
2466
2512
  if (action === "remove" && !(await askConfirm("Remove skill", 'Remove the external skill "' + name + '" from all agents? This deletes it from disk.', true))) return;
2467
2513
  postSkill(name, action);
2468
2514
  });
2515
+ // Skill-related settings live in this tab too; reuse the settings POST, with the note here.
2516
+ $("skillSettings").addEventListener("click", (e) => {
2517
+ const t = e.target.closest("button.toggle[data-skey]");
2518
+ if (t) postSetting(t.dataset.skey, t.dataset.on !== "1", t, $("skillsNote"));
2519
+ });
2520
+ $("skillSettings").addEventListener("change", (e) => {
2521
+ const sel = e.target.closest("select.choice[data-skey]");
2522
+ if (sel) postSetting(sel.dataset.skey, sel.value, sel, $("skillsNote"));
2523
+ });
2469
2524
  $("seSave").addEventListener("click", saveEditor);
2470
2525
  $("seCancel").addEventListener("click", closeEditor);
2471
2526
  $("skillsCheck").addEventListener("click", () => postSkill("*", "check-updates"));
2472
2527
  }
2473
2528
 
2529
+ // The skill-related settings surfaced inside the Skills tab (a better-organised dashboard
2530
+ // than burying them under Settings). They still live in the one settings registry.
2531
+ const SKILL_SETTING_KEYS = ["remote-skills", "skill-update-policy", "auto-sync"];
2532
+ function renderSkillSettings(cats) {
2533
+ const all = (cats || []).flatMap((c) => c.settings || []);
2534
+ const rows = SKILL_SETTING_KEYS.map((k) => all.find((s) => s.key === k)).filter(Boolean);
2535
+ $("skillSettings").innerHTML = rows.length ? rows.map(settingRow).join("")
2536
+ : '<div class="empty" style="padding:8px 0">No skill settings available.</div>';
2537
+ }
2538
+ async function loadSkillSettings() {
2539
+ try { renderSkillSettings((await (await fetch("/api/settings", { cache: "no-store" })).json()).categories || []); }
2540
+ catch { $("skillSettings").innerHTML = ""; }
2541
+ }
2542
+
2474
2543
  let skillsLoaded = false;
2475
2544
  async function loadSkills() {
2545
+ loadSkillSettings();
2476
2546
  try {
2477
2547
  const res = await fetch("/api/skills", { cache: "no-store" });
2478
2548
  renderSkills((await res.json()).skills || []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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": [