@enigmax/dashboard 0.1.63 → 0.1.65
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.
- package/assets/index.html +216 -8
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -321,6 +321,12 @@
|
|
|
321
321
|
/* Per-memory delete in the Recall list: kept to the right and revealed on row hover/focus. */
|
|
322
322
|
.recall-del { margin-left: auto; opacity: 0; transition: opacity .12s; }
|
|
323
323
|
.set-row:hover .recall-del, .recall-del:focus-visible { opacity: 1; }
|
|
324
|
+
/* Recall multi-select: a checkbox per row, a highlight for selected rows, and a bulk-action
|
|
325
|
+
bar that appears only while something is selected. Ctrl/Cmd+click toggles, Shift+click ranges. */
|
|
326
|
+
.rc-check { flex: 0 0 auto; width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); margin: 0; }
|
|
327
|
+
.set-row.sel { background: rgba(224, 164, 88, .10); }
|
|
328
|
+
.rc-bulk { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
329
|
+
.rc-bulk .rc-seln { font-size: 12px; color: var(--muted); }
|
|
324
330
|
/* Per-agent skill toggles: one pill per app (the app's icon), green when the skill is on for it; the app name is the tooltip. */
|
|
325
331
|
.set-agents { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
326
332
|
.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; }
|
|
@@ -454,6 +460,10 @@
|
|
|
454
460
|
.modal-fl { display: block; margin-top: 14px; margin-bottom: 5px; font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
455
461
|
.modal-fl .modal-opt { text-transform: none; letter-spacing: 0; opacity: .75; }
|
|
456
462
|
.modal-fl + .modal-input { margin-top: 0; }
|
|
463
|
+
/* Provider-override editor labels + key hint. */
|
|
464
|
+
.prov-l { display: block; margin-top: 12px; margin-bottom: 4px; font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
465
|
+
.prov-hint { margin-top: 8px; font-size: 12px; color: var(--muted); word-break: break-word; }
|
|
466
|
+
.prov-hint a { color: var(--accent); }
|
|
457
467
|
/* Usage view provider/account pickers. */
|
|
458
468
|
.useg-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
|
|
459
469
|
.useg-pick select { width: auto; min-width: 170px; }
|
|
@@ -516,6 +526,7 @@
|
|
|
516
526
|
<a href="#/settings" data-view="settings" class="tab">Settings</a>
|
|
517
527
|
<a href="#/resources" data-view="resources" class="tab">Resources</a>
|
|
518
528
|
<a href="#/recall" data-view="recall" class="tab">Recall</a>
|
|
529
|
+
<a href="#/packs" data-view="packs" class="tab">Packs</a>
|
|
519
530
|
</nav>
|
|
520
531
|
<div class="nav">
|
|
521
532
|
<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>
|
|
@@ -793,6 +804,18 @@
|
|
|
793
804
|
<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>
|
|
794
805
|
</section>
|
|
795
806
|
|
|
807
|
+
<section id="view-packs" style="display:none">
|
|
808
|
+
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
809
|
+
<div style="flex:1">
|
|
810
|
+
<h1>Packs</h1>
|
|
811
|
+
<p>Optional harness packs you can add on demand. Each runs in its own <strong>isolated agent context</strong>, so its skills and commands never load into your normal coding agent. Launch a pack from a terminal with <code>enigma <pack></code>.</p>
|
|
812
|
+
</div>
|
|
813
|
+
<button id="packsRefresh" 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>
|
|
814
|
+
</div>
|
|
815
|
+
<div id="packsNote" class="set-note"></div>
|
|
816
|
+
<div id="packsBody"><div class="empty" style="padding:12px 0">Loading...</div></div>
|
|
817
|
+
</section>
|
|
818
|
+
|
|
796
819
|
<section id="view-recall" style="display:none">
|
|
797
820
|
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
798
821
|
<div style="flex:1">
|
|
@@ -3460,7 +3483,7 @@ export {
|
|
|
3460
3483
|
|
|
3461
3484
|
// --- accounts & profiles subpage (mirrors the TUI hub over /api/accounts) ---
|
|
3462
3485
|
let accountsLoaded = false;
|
|
3463
|
-
let accTools = [], accAccounts = [], accProfiles = [];
|
|
3486
|
+
let accTools = [], accAccounts = [], accProfiles = [], accPresets = [];
|
|
3464
3487
|
// Real-time name validator mirroring the server (accounts.ts validateAccountName + the
|
|
3465
3488
|
// duplicate checks), so the modal blocks an invalid or already-used name as the user types.
|
|
3466
3489
|
// `taken` = names already in scope (per-tool for accounts, global for profiles); `current`
|
|
@@ -3483,16 +3506,23 @@ export {
|
|
|
3483
3506
|
+ (a.loggedIn ? '<span class="tag">' + esc(a.email || "logged in") + "</span>" : '<span class="tag warn">not logged in</span>');
|
|
3484
3507
|
const btn = (act, label, cls) => '<button type="button" class="' + (cls || "toggle") + '" data-acc="' + act + '" data-tool="' + esc(a.tool) + '" data-name="' + esc(a.name) + '">' + label + "</button>";
|
|
3485
3508
|
const removeBtn = '<button type="button" class="btn-danger icon" title="Remove" aria-label="Remove account ' + esc(a.name) + '" data-acc="remove" data-tool="' + esc(a.tool) + '" data-name="' + esc(a.name) + '">' + TRASH_ICON + "</button>";
|
|
3509
|
+
// Provider override (e.g. MiniMax) only for tools that support it, and never the
|
|
3510
|
+
// built-in "default" account (kept on the vendor default). Managed accounts only.
|
|
3511
|
+
const provBtn = (a.supportsProvider && a.removable) ? btn("provider", "Provider") : "";
|
|
3486
3512
|
const acts = '<div class="set-ctl" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
|
|
3487
3513
|
+ (a.active ? "" : btn("activate", "Use"))
|
|
3488
3514
|
+ (a.loggedIn ? "" : btn("login", "Log in..."))
|
|
3515
|
+
+ provBtn
|
|
3489
3516
|
+ (a.removable ? btn("rename", "Rename") + removeBtn : "")
|
|
3490
3517
|
+ "</div>";
|
|
3491
3518
|
const toolIc = agentSvg(a.tool);
|
|
3492
3519
|
const toolBadge = toolIc
|
|
3493
3520
|
? '<span class="aicon set-ic" title="' + esc(a.toolLabel) + '" aria-label="' + esc(a.toolLabel) + '">' + toolIc + "</span>"
|
|
3494
3521
|
: '<span class="tag ext">' + esc(a.toolLabel) + "</span> ";
|
|
3495
|
-
|
|
3522
|
+
const provTag = a.provider
|
|
3523
|
+
? ' <span class="tag meas" title="' + esc(a.provider.baseUrl) + '">via ' + esc(a.provider.preset && a.provider.preset !== "custom" ? a.provider.preset : "custom") + (a.provider.model ? " · " + esc(a.provider.model) : "") + "</span>"
|
|
3524
|
+
: "";
|
|
3525
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(a.name) + " " + toolBadge + badges + provTag
|
|
3496
3526
|
+ '</div><div class="set-hint">' + esc(a.dir) + "</div></div>" + acts + "</div>";
|
|
3497
3527
|
}
|
|
3498
3528
|
function profileRow(p, tools, accounts) {
|
|
@@ -3515,7 +3545,7 @@ export {
|
|
|
3515
3545
|
function renderAccounts(d) {
|
|
3516
3546
|
d = d || {};
|
|
3517
3547
|
const accounts = d.accounts || [], profiles = d.profiles || [], tools = d.tools || [];
|
|
3518
|
-
accTools = tools; accAccounts = accounts; accProfiles = profiles;
|
|
3548
|
+
accTools = tools; accAccounts = accounts; accProfiles = profiles; accPresets = d.presets || [];
|
|
3519
3549
|
$("accountsBody").innerHTML = accounts.length ? accounts.map(accountRow).join("") : '<div class="empty" style="padding:20px 0">No accounts.</div>';
|
|
3520
3550
|
$("profilesBody").innerHTML = profiles.length ? profiles.map((p) => profileRow(p, tools, accounts)).join("")
|
|
3521
3551
|
: '<div class="empty" style="padding:20px 0">No profiles yet. Add one to pin an account per tool.</div>';
|
|
@@ -3534,6 +3564,69 @@ export {
|
|
|
3534
3564
|
setNote("accountsNote", "Error: " + (out.error || "failed"), "err");
|
|
3535
3565
|
} catch { if (rollback) rollback(); setNote("accountsNote", "Could not reach the server.", "err"); }
|
|
3536
3566
|
}
|
|
3567
|
+
// Provider-override editor: a small multi-field modal (backend select + base/model/token)
|
|
3568
|
+
// since openModal only does single fields. Resolves to { clear:true } to drop the override,
|
|
3569
|
+
// an object to set it ({ baseUrl, model?, preset?, token? } - token only when typed), or null.
|
|
3570
|
+
function editProvider(a) {
|
|
3571
|
+
return new Promise((resolve) => {
|
|
3572
|
+
const toolPresets = accPresets.filter((p) => p.tool === a.tool);
|
|
3573
|
+
const cur = a.provider; // { baseUrl, model?, preset?, hasToken } | null
|
|
3574
|
+
const opts = ['<option value="default">Default (Anthropic)</option>']
|
|
3575
|
+
.concat(toolPresets.map((p) => '<option value="' + esc(p.id) + '">' + esc(p.label) + "</option>"))
|
|
3576
|
+
.concat(['<option value="custom">Custom (Anthropic-compatible)</option>']).join("");
|
|
3577
|
+
const back = document.createElement("div");
|
|
3578
|
+
back.className = "modal-back";
|
|
3579
|
+
back.innerHTML = '<div class="modal" role="dialog" aria-modal="true" style="max-width:480px;text-align:left">'
|
|
3580
|
+
+ '<div class="modal-title">Provider for ' + esc(a.name) + "</div>"
|
|
3581
|
+
+ '<div class="modal-msg">Point ' + esc(a.toolLabel) + " at another Anthropic-compatible backend (e.g. MiniMax), or keep the default.</div>"
|
|
3582
|
+
+ '<label class="prov-l">Backend</label><select class="modal-input" id="provSel">' + opts + "</select>"
|
|
3583
|
+
+ '<div id="provFields" style="margin-top:8px">'
|
|
3584
|
+
+ '<label class="prov-l">Base URL</label><input class="modal-input" id="provBase" placeholder="https://...">'
|
|
3585
|
+
+ '<label class="prov-l">Model</label><input class="modal-input" id="provModel" placeholder="(provider default)">'
|
|
3586
|
+
+ '<label class="prov-l">API key</label><input class="modal-input" id="provToken" type="password" autocomplete="off" placeholder="' + (cur && cur.hasToken ? "set - leave blank to keep" : "API key") + '">'
|
|
3587
|
+
+ '<div class="prov-hint" id="provHint"></div></div>'
|
|
3588
|
+
+ '<div class="modal-acts"><button type="button" class="toggle modal-cancel">Cancel</button>'
|
|
3589
|
+
+ '<button type="button" class="toggle on modal-ok">Save</button></div></div>';
|
|
3590
|
+
document.body.appendChild(back);
|
|
3591
|
+
requestAnimationFrame(() => back.classList.add("show"));
|
|
3592
|
+
const sel = back.querySelector("#provSel"), fields = back.querySelector("#provFields");
|
|
3593
|
+
const base = back.querySelector("#provBase"), model = back.querySelector("#provModel"), token = back.querySelector("#provToken"), hint = back.querySelector("#provHint");
|
|
3594
|
+
sel.value = cur ? (cur.preset && cur.preset !== "custom" ? cur.preset : "custom") : "default";
|
|
3595
|
+
function applySel() {
|
|
3596
|
+
const v = sel.value;
|
|
3597
|
+
fields.style.display = v === "default" ? "none" : "";
|
|
3598
|
+
if (v === "default") return;
|
|
3599
|
+
const preset = toolPresets.find((p) => p.id === v);
|
|
3600
|
+
if (preset) {
|
|
3601
|
+
base.value = (cur && cur.preset === v && cur.baseUrl) || preset.baseUrl;
|
|
3602
|
+
model.value = (cur && cur.preset === v && cur.model) || preset.model || "";
|
|
3603
|
+
hint.innerHTML = preset.tokenUrl ? 'Get a key at <a href="' + esc(preset.tokenUrl) + '" target="_blank" rel="noopener">' + esc(preset.tokenUrl) + "</a>" : "";
|
|
3604
|
+
} else {
|
|
3605
|
+
base.value = (cur && (!cur.preset || cur.preset === "custom") && cur.baseUrl) || "";
|
|
3606
|
+
model.value = (cur && cur.model) || "";
|
|
3607
|
+
hint.textContent = "";
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
applySel();
|
|
3611
|
+
sel.addEventListener("change", applySel);
|
|
3612
|
+
let done = false;
|
|
3613
|
+
const close = (val) => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); resolve(val); };
|
|
3614
|
+
back.querySelector(".modal-cancel").addEventListener("click", () => close(null));
|
|
3615
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back) close(null); });
|
|
3616
|
+
back.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); close(null); } });
|
|
3617
|
+
back.querySelector(".modal-ok").addEventListener("click", () => {
|
|
3618
|
+
if (sel.value === "default") { close({ clear: true }); return; }
|
|
3619
|
+
const b = base.value.trim();
|
|
3620
|
+
if (!/^https?:\/\//i.test(b)) { hint.textContent = "Enter a base URL starting with http:// or https://"; return; }
|
|
3621
|
+
const out = { baseUrl: b };
|
|
3622
|
+
if (model.value.trim()) out.model = model.value.trim();
|
|
3623
|
+
if (sel.value !== "custom") out.preset = sel.value;
|
|
3624
|
+
if (token.value) out.token = token.value; // only send a typed key (blank keeps the stored one)
|
|
3625
|
+
close(out);
|
|
3626
|
+
});
|
|
3627
|
+
sel.focus();
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3537
3630
|
function wireAccounts() {
|
|
3538
3631
|
$("accAdd").addEventListener("click", async () => {
|
|
3539
3632
|
const tool = accTools.length === 1 ? accTools[0].name
|
|
@@ -3551,6 +3644,12 @@ export {
|
|
|
3551
3644
|
else if (op === "remove") { if (await askConfirm("Remove account", "Remove account '" + name + "' and delete its local config dir?", true)) accAction("account.remove", { tool, name }); }
|
|
3552
3645
|
else if (op === "rename") { const taken = accAccounts.filter((a) => a.tool === tool).map((a) => a.name); const nn = await askText("Rename account", name, "Rename '" + name + "' to:", nameValidator(taken, name)); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
|
|
3553
3646
|
else if (op === "login") { setNote("accountsNote", "To log in, run this in a terminal: enigma " + tool + " " + name); }
|
|
3647
|
+
else if (op === "provider") {
|
|
3648
|
+
const a = accAccounts.find((x) => x.tool === tool && x.name === name);
|
|
3649
|
+
if (!a) return;
|
|
3650
|
+
const out = await editProvider(a);
|
|
3651
|
+
if (out) accAction("account.provider", { tool, name, provider: out.clear ? null : out });
|
|
3652
|
+
}
|
|
3554
3653
|
});
|
|
3555
3654
|
$("profilesBody").addEventListener("click", async (e) => {
|
|
3556
3655
|
const b = e.target.closest("button[data-prof-act]"); if (!b) return;
|
|
@@ -3610,7 +3709,7 @@ export {
|
|
|
3610
3709
|
|
|
3611
3710
|
// --- hash routing between the subpages ---
|
|
3612
3711
|
// Single source of truth for the subpage ids (savings is the default/fallback).
|
|
3613
|
-
const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources", "recall"];
|
|
3712
|
+
const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources", "recall", "packs"];
|
|
3614
3713
|
// Views that apply to a single project; the rest are global/machine-level only.
|
|
3615
3714
|
const PROJECT_VIEWS = ["settings", "skills"];
|
|
3616
3715
|
let activeView = "savings";
|
|
@@ -3631,6 +3730,7 @@ export {
|
|
|
3631
3730
|
else if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
|
|
3632
3731
|
else if (v === "resources") loadResources();
|
|
3633
3732
|
else if (v === "recall") loadRecall();
|
|
3733
|
+
else if (v === "packs") loadPacks();
|
|
3634
3734
|
// The chart was sized while its view may have been hidden; nudge it on return.
|
|
3635
3735
|
else if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
|
|
3636
3736
|
}
|
|
@@ -3690,6 +3790,52 @@ export {
|
|
|
3690
3790
|
});
|
|
3691
3791
|
}
|
|
3692
3792
|
|
|
3793
|
+
// --- packs subpage (optional isolated harness packs; marketplace) ---
|
|
3794
|
+
function renderPacks(list) {
|
|
3795
|
+
list = list || [];
|
|
3796
|
+
if (!list.length) { $("packsBody").innerHTML = '<div class="empty" style="padding:12px 0">No packs available.</div>'; return; }
|
|
3797
|
+
$("packsBody").innerHTML = list.map((p) => {
|
|
3798
|
+
const tags = (p.tags || []).map((t) => '<span class="tag">' + esc(t) + '</span>').join(" ");
|
|
3799
|
+
const state = p.installed ? '<span class="tag">' + (p.version ? "installed " + esc(p.version) : "installed") + '</span>' : '<span class="tag">not installed</span>';
|
|
3800
|
+
const added = p.enabled ? '<span class="tag" style="color:var(--ok)">added</span>' : "";
|
|
3801
|
+
const actions = p.installed
|
|
3802
|
+
? '<button type="button" class="toggle on" data-pack="launch" data-id="' + esc(p.id) + '">Launch command</button>'
|
|
3803
|
+
+ '<button type="button" class="toggle" data-pack="setup" data-id="' + esc(p.id) + '" title="Register the pack MCP servers (needs Python 3)">Setup MCP</button>'
|
|
3804
|
+
+ '<button type="button" class="toggle" data-pack="update" data-id="' + esc(p.id) + '">Update</button>'
|
|
3805
|
+
+ '<button type="button" class="btn-danger" data-pack="remove" data-id="' + esc(p.id) + '">Remove</button>'
|
|
3806
|
+
: '<button type="button" class="toggle on" data-pack="install" data-id="' + esc(p.id) + '">Install</button>';
|
|
3807
|
+
return '<div class="panel"><div class="set-row"><div class="set-meta">'
|
|
3808
|
+
+ '<div class="set-name">' + esc(p.label) + " " + state + " " + added + '</div>'
|
|
3809
|
+
+ '<div class="set-hint">' + esc(p.description) + '</div>'
|
|
3810
|
+
+ '<div class="set-hint" style="margin-top:6px">' + tags + ' <a href="' + esc(p.homepage) + '" target="_blank" rel="noopener">source</a></div>'
|
|
3811
|
+
+ '</div></div><div class="set-ctl" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">' + actions + '</div></div>';
|
|
3812
|
+
}).join("");
|
|
3813
|
+
}
|
|
3814
|
+
async function loadPacks() {
|
|
3815
|
+
$("packsNote").textContent = "";
|
|
3816
|
+
try { renderPacks((await (await fetch("/api/packs", { cache: "no-store" })).json()).packs); }
|
|
3817
|
+
catch { $("packsBody").innerHTML = '<div class="empty" style="padding:12px 0">Packs unavailable.</div>'; }
|
|
3818
|
+
}
|
|
3819
|
+
async function packAction(id, action) {
|
|
3820
|
+
const confirmMsg = action === "remove" ? "Remove this pack and its isolated context?" : null;
|
|
3821
|
+
if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, true))) return;
|
|
3822
|
+
setNote("packsNote", action === "install" || action === "update" ? "Working..." : "Saving...");
|
|
3823
|
+
try {
|
|
3824
|
+
const out = await (await fetch("/api/packs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, action }) })).json();
|
|
3825
|
+
if (out.packs) renderPacks(out.packs);
|
|
3826
|
+
if (out.command) { try { await navigator.clipboard.writeText(out.command); } catch { /* clipboard blocked */ } }
|
|
3827
|
+
setNote("packsNote", out.note || out.error || (out.ok ? "Done." : "Action failed."), out.ok ? "ok" : "err");
|
|
3828
|
+
} catch { setNote("packsNote", "Could not reach the server.", "err"); }
|
|
3829
|
+
}
|
|
3830
|
+
function wirePacks() {
|
|
3831
|
+
$("packsRefresh").addEventListener("click", loadPacks);
|
|
3832
|
+
$("view-packs").addEventListener("click", (e) => {
|
|
3833
|
+
const b = e.target.closest("button[data-pack]");
|
|
3834
|
+
if (!b) return;
|
|
3835
|
+
packAction(b.dataset.id, b.dataset.pack);
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3693
3839
|
// --- recall subpage (local session memory; reads your own transcripts) ---
|
|
3694
3840
|
const RECALL_TYPES = ["bugfix", "feature", "refactor", "change", "discovery", "decision", "security"];
|
|
3695
3841
|
let recallProjSig = "";
|
|
@@ -3765,6 +3911,9 @@ export {
|
|
|
3765
3911
|
llmBtn.setAttribute("aria-checked", p.llm ? "true" : "false");
|
|
3766
3912
|
}
|
|
3767
3913
|
let recallItemsById = {};
|
|
3914
|
+
// Multi-select state for the Recall list: a Set of selected ids, the ordered ids as
|
|
3915
|
+
// rendered (for Shift range selection), and the anchor index for the next Shift+click.
|
|
3916
|
+
let recallSel = new Set(), recallOrderIds = [], recallAnchorIdx = null;
|
|
3768
3917
|
function recallRowHtml(it, anchorId) {
|
|
3769
3918
|
const ic = '<span class="aicon" title="' + esc(it.source) + '" aria-label="' + esc(it.source) + '">' + (agentSvg(it.source) || esc(it.source)) + "</span>";
|
|
3770
3919
|
const date = it.createdAt ? new Date(it.createdAt).toISOString().slice(0, 10) : "";
|
|
@@ -3786,13 +3935,45 @@ export {
|
|
|
3786
3935
|
}
|
|
3787
3936
|
function renderRecallList(items) {
|
|
3788
3937
|
recallItemsById = {};
|
|
3938
|
+
// A fresh list invalidates the previous selection (ids may have changed).
|
|
3939
|
+
recallSel = new Set(); recallOrderIds = []; recallAnchorIdx = null;
|
|
3789
3940
|
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; }
|
|
3790
|
-
|
|
3791
|
-
|
|
3941
|
+
const rows = items.map((it) => {
|
|
3942
|
+
// data-idx aligns with recallOrderIds (id'd rows only) so Shift ranges are exact.
|
|
3943
|
+
let dataIdx = "";
|
|
3944
|
+
if (it.id) { recallItemsById[it.id] = it; recallOrderIds.push(it.id); dataIdx = recallOrderIds.length - 1; }
|
|
3792
3945
|
const del = it.id ? '<button type="button" class="btn-danger icon recall-del" title="Delete memory" aria-label="Delete this memory" data-del="' + it.id + '">' + TRASH_ICON + "</button>" : "";
|
|
3793
|
-
|
|
3946
|
+
const check = it.id ? '<input type="checkbox" class="rc-check" data-id="' + it.id + '" aria-label="Select this memory">' : '<span style="width:15px;flex:0 0 auto"></span>';
|
|
3947
|
+
return '<div class="set-row" style="cursor:pointer" data-rid="' + (it.id || "") + '"' + (it.id ? ' data-idx="' + dataIdx + '"' : "") + '>' + check + recallRowHtml(it) + del + "</div>"
|
|
3794
3948
|
+ '<div id="rd-' + (it.id || "") + '" style="display:none"></div>';
|
|
3795
|
-
}).join("")
|
|
3949
|
+
}).join("");
|
|
3950
|
+
// Bulk-action bar (hidden until something is selected) + the rows.
|
|
3951
|
+
$("recallList").innerHTML = '<div id="recallBulk" class="rc-bulk" style="display:none">'
|
|
3952
|
+
+ '<span class="rc-seln" id="recallSelN"></span>'
|
|
3953
|
+
+ '<button type="button" class="btn-danger" data-bulk-del>Delete selected</button>'
|
|
3954
|
+
+ '<button type="button" class="toggle" data-bulk-clear>Clear selection</button></div>'
|
|
3955
|
+
+ '<div class="panel">' + rows + "</div>";
|
|
3956
|
+
}
|
|
3957
|
+
/** Reflect the current selection in the DOM: row highlight, checkboxes, and the bulk bar. */
|
|
3958
|
+
function updateRecallSelUI() {
|
|
3959
|
+
document.querySelectorAll("#recallList .set-row[data-rid]").forEach((row) => {
|
|
3960
|
+
const id = Number(row.dataset.rid);
|
|
3961
|
+
const on = recallSel.has(id);
|
|
3962
|
+
row.classList.toggle("sel", on);
|
|
3963
|
+
const cb = row.querySelector(".rc-check");
|
|
3964
|
+
if (cb) cb.checked = on;
|
|
3965
|
+
});
|
|
3966
|
+
const bar = $("recallBulk");
|
|
3967
|
+
if (bar) {
|
|
3968
|
+
bar.style.display = recallSel.size ? "" : "none";
|
|
3969
|
+
const n = $("recallSelN");
|
|
3970
|
+
if (n) n.textContent = recallSel.size + (recallSel.size === 1 ? " memory selected" : " memories selected");
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
/** Select every id between two row indices (inclusive), for Shift+click range selection. */
|
|
3974
|
+
function recallSelectRange(fromIdx, toIdx) {
|
|
3975
|
+
const lo = Math.min(fromIdx, toIdx), hi = Math.max(fromIdx, toIdx);
|
|
3976
|
+
for (let i = lo; i <= hi; i++) { const id = recallOrderIds[i]; if (id) recallSel.add(id); }
|
|
3796
3977
|
}
|
|
3797
3978
|
async function loadRecallTimeline(id) {
|
|
3798
3979
|
const el = $("rtl-" + id);
|
|
@@ -3887,6 +4068,16 @@ export {
|
|
|
3887
4068
|
$("recallType").addEventListener("change", loadRecall);
|
|
3888
4069
|
$("recallProject").addEventListener("change", loadRecall);
|
|
3889
4070
|
$("recallList").addEventListener("click", async (e) => {
|
|
4071
|
+
// Bulk actions on the current multi-selection.
|
|
4072
|
+
if (e.target.closest("[data-bulk-clear]")) { recallSel.clear(); recallAnchorIdx = null; updateRecallSelUI(); return; }
|
|
4073
|
+
if (e.target.closest("[data-bulk-del]")) {
|
|
4074
|
+
const ids = [...recallSel];
|
|
4075
|
+
if (!ids.length) return;
|
|
4076
|
+
const noun = ids.length === 1 ? "memory" : "memories";
|
|
4077
|
+
if (!(await askConfirm("Delete memories", "Delete " + ids.length + " selected " + noun + "? This cannot be undone.", true))) return;
|
|
4078
|
+
recallOpPost({ op: "delete", ids }, "Deleting " + ids.length + "...", ids.length + " " + noun + " deleted.");
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
3890
4081
|
const del = e.target.closest("[data-del]");
|
|
3891
4082
|
if (del) {
|
|
3892
4083
|
if (!(await askConfirm("Delete memory", "Delete this memory? This cannot be undone.", true))) return;
|
|
@@ -3897,6 +4088,22 @@ export {
|
|
|
3897
4088
|
if (tl) { loadRecallTimeline(tl.dataset.tl); return; }
|
|
3898
4089
|
const row = e.target.closest("[data-rid]");
|
|
3899
4090
|
if (!row || !row.dataset.rid) return;
|
|
4091
|
+
const id = Number(row.dataset.rid);
|
|
4092
|
+
const cb = e.target.closest(".rc-check");
|
|
4093
|
+
// Selection shortcuts: a checkbox click, Ctrl/Cmd+click (toggle one), or Shift+click
|
|
4094
|
+
// (select the range from the last-clicked anchor through this row, inclusive).
|
|
4095
|
+
if (cb || e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
4096
|
+
const idx = row.dataset.idx === undefined ? -1 : Number(row.dataset.idx);
|
|
4097
|
+
if (e.shiftKey && recallAnchorIdx !== null && idx >= 0) {
|
|
4098
|
+
recallSelectRange(recallAnchorIdx, idx);
|
|
4099
|
+
} else {
|
|
4100
|
+
if (recallSel.has(id)) recallSel.delete(id); else recallSel.add(id);
|
|
4101
|
+
if (idx >= 0) recallAnchorIdx = idx;
|
|
4102
|
+
}
|
|
4103
|
+
updateRecallSelUI();
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
// Plain click expands/collapses the detail (unchanged).
|
|
3900
4107
|
const detail = $("rd-" + row.dataset.rid);
|
|
3901
4108
|
if (!detail) return;
|
|
3902
4109
|
if (detail.innerHTML === "") detail.innerHTML = recallDetailHtml(recallItemsById[row.dataset.rid]);
|
|
@@ -4197,6 +4404,7 @@ export {
|
|
|
4197
4404
|
wireUsage();
|
|
4198
4405
|
wireResources();
|
|
4199
4406
|
wireRecall();
|
|
4407
|
+
wirePacks();
|
|
4200
4408
|
wireScope();
|
|
4201
4409
|
enhanceAll(); // upgrade every static <select> into the shared pretty dropdown
|
|
4202
4410
|
$("cfgExport").addEventListener("click", exportConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.65",
|
|
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": [
|