@iamem/amem 0.1.2 → 0.1.3
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/README.md +64 -1
- package/dist/api/routes.js +331 -1
- package/dist/attest.d.ts +13 -0
- package/dist/attest.js +44 -0
- package/dist/cli.js +241 -2
- package/dist/context.d.ts +10 -1
- package/dist/context.js +105 -3
- package/dist/db.d.ts +141 -0
- package/dist/db.js +398 -0
- package/dist/embed.js +5 -14
- package/dist/hook.js +8 -1
- package/dist/hygiene.d.ts +1 -2
- package/dist/hygiene.js +1 -2
- package/dist/install/hosts.js +8 -0
- package/dist/install/skills.js +10 -5
- package/dist/license.d.ts +1 -0
- package/dist/license.js +21 -19
- package/dist/mcp.js +221 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +16 -1
- package/dist/skill-capture.d.ts +43 -0
- package/dist/skill-capture.js +146 -0
- package/dist/skills.d.ts +106 -0
- package/dist/skills.js +422 -0
- package/docs/backlog.md +9 -0
- package/package.json +1 -1
- package/skills/amem-tasks/SKILL.md +100 -0
- package/skills/amem-write-skill/SKILL.md +99 -0
- package/templates/cursor-rule.mdc +16 -6
- package/templates/policy.deny-default.toml +5 -0
- package/templates/policy.example.toml +8 -0
- package/ui-static/app.js +435 -227
- package/ui-static/index.html +11 -34
- package/ui-static/styles.css +299 -0
package/ui-static/app.js
CHANGED
|
@@ -4,7 +4,7 @@ const PLATFORM_STORAGE = "amem.selectedPlatforms";
|
|
|
4
4
|
const WELCOME_STORAGE = "amem.welcome.dismissed";
|
|
5
5
|
const BRAIN_SCOPE_STORAGE = "amem.brain.scope";
|
|
6
6
|
const MEMORY_VIZ_STORAGE = "amem.memory.viz";
|
|
7
|
-
const TABS = ["
|
|
7
|
+
const TABS = ["setup", "dashboard", "brain", "analytics", "stats", "tasks", "skills"];
|
|
8
8
|
const VIZ_MODES = ["map", "blocks", "orbit"];
|
|
9
9
|
const FALLBACK_CLIENTS = [
|
|
10
10
|
{ id: "cursor", label: "Cursor", hint: "Rules, skills, hooks" },
|
|
@@ -83,26 +83,14 @@ function dismissWelcome() {
|
|
|
83
83
|
|
|
84
84
|
function initialTab() {
|
|
85
85
|
const fromUrl = initialUrl.get("tab");
|
|
86
|
-
// Paid users: never force the sell card on every launch.
|
|
87
|
-
if (isPaidLicenseCached()) {
|
|
88
|
-
if (fromUrl === "stats") return "analytics";
|
|
89
|
-
if (TABS.includes(fromUrl)) return fromUrl;
|
|
90
|
-
return "dashboard";
|
|
91
|
-
}
|
|
92
|
-
if (!welcomeDismissed() && fromUrl !== "brain" && fromUrl !== "stats" && fromUrl !== "analytics" && fromUrl !== "dashboard") return "welcome";
|
|
93
86
|
if (fromUrl === "stats") return "analytics";
|
|
94
87
|
if (TABS.includes(fromUrl)) return fromUrl;
|
|
95
|
-
return "
|
|
88
|
+
return "dashboard";
|
|
96
89
|
}
|
|
97
90
|
|
|
98
|
-
/**
|
|
91
|
+
/** All features are free and unlocked. */
|
|
99
92
|
function isPaidLicenseCached() {
|
|
100
|
-
|
|
101
|
-
const raw = localStorage.getItem("amem.license.tier");
|
|
102
|
-
return raw === "pro" || raw === "it";
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
93
|
+
return true;
|
|
106
94
|
}
|
|
107
95
|
|
|
108
96
|
function persistLicenseTier(tier) {
|
|
@@ -218,6 +206,9 @@ const state = {
|
|
|
218
206
|
selectedNode: null,
|
|
219
207
|
activeEventId: null,
|
|
220
208
|
brainFilter: "files",
|
|
209
|
+
skills: [],
|
|
210
|
+
skillDrafts: [],
|
|
211
|
+
skillsDir: "",
|
|
221
212
|
draftSelected: {},
|
|
222
213
|
selectedFile: null,
|
|
223
214
|
brainSearch: "",
|
|
@@ -327,6 +318,12 @@ function setTab(tab) {
|
|
|
327
318
|
persistBrainAll(true);
|
|
328
319
|
state.brainError = null;
|
|
329
320
|
resetBrainView();
|
|
321
|
+
} else if (tab === "tasks") {
|
|
322
|
+
// Agents file tasks against whatever repo they were working in, which is rarely the
|
|
323
|
+
// folder the UI was launched from. Default to every memory or the board looks empty.
|
|
324
|
+
state.brainAll = true;
|
|
325
|
+
persistBrainAll(true);
|
|
326
|
+
stopAllViz();
|
|
330
327
|
} else {
|
|
331
328
|
stopAllViz();
|
|
332
329
|
}
|
|
@@ -451,19 +448,6 @@ function paintVault() {
|
|
|
451
448
|
? `Last backup: ${last.name} · ${last.encrypted ? "encrypted" : "plaintext"} · stays in ${state.vault.backup.dir}`
|
|
452
449
|
: `Backups go to ${state.vault.backup?.dir || "~/.amem/backups"} on this machine.`;
|
|
453
450
|
}
|
|
454
|
-
const shop = state.shop;
|
|
455
|
-
const row = $("#shopRow");
|
|
456
|
-
if (row) row.classList.toggle("hidden", shop && shop.enabled === false);
|
|
457
|
-
const pro = $("#buyPro");
|
|
458
|
-
const it = $("#buyIt");
|
|
459
|
-
if (pro && shop?.proUrl) {
|
|
460
|
-
pro.setAttribute("href", shop.proUrl);
|
|
461
|
-
pro.textContent = `Buy Pro · ${shop.proPrice || "$12"}`;
|
|
462
|
-
}
|
|
463
|
-
if (it && shop?.itUrl) {
|
|
464
|
-
it.setAttribute("href", shop.itUrl);
|
|
465
|
-
it.textContent = `Buy IT · ${shop.itPrice || "$49"}`;
|
|
466
|
-
}
|
|
467
451
|
}
|
|
468
452
|
|
|
469
453
|
async function focusPersonal() {
|
|
@@ -810,89 +794,8 @@ function defaultProposal() {
|
|
|
810
794
|
}
|
|
811
795
|
|
|
812
796
|
function renderWelcome() {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
renderPageInsight();
|
|
816
|
-
if (isPaidLicense()) {
|
|
817
|
-
dismissWelcome();
|
|
818
|
-
setTab("dashboard");
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
const shop = state.shop || {};
|
|
822
|
-
const proUrl = shop.proUrl || "https://getamem.com/buy/pro";
|
|
823
|
-
const itUrl = shop.itUrl || "https://getamem.com/buy/it";
|
|
824
|
-
const proPrice = shop.proPrice || "$12";
|
|
825
|
-
const itPrice = shop.itPrice || "$49";
|
|
826
|
-
const tier = state.license?.tier || "free";
|
|
827
|
-
const shopOn = shop.enabled !== false;
|
|
828
|
-
|
|
829
|
-
main.innerHTML = `
|
|
830
|
-
<section class="hero welcome-hero">
|
|
831
|
-
<div class="hero-inner welcome-wide">
|
|
832
|
-
<div class="welcome-brand">
|
|
833
|
-
<div class="welcome-logo" aria-hidden="true">a</div>
|
|
834
|
-
<h1>amem</h1>
|
|
835
|
-
</div>
|
|
836
|
-
<p>Local agent memory for Cursor and any MCP host. Facts stay in <code>~/.amem</code> on this machine. Free already remembers; Pro = better local ranking + cleanup + rules sync. IT = richer attest for security/DevEx — not more memory.</p>
|
|
837
|
-
<div class="plan-grid">
|
|
838
|
-
<article class="plan-card ${tier === "free" ? "current" : ""}">
|
|
839
|
-
<h2>Free</h2>
|
|
840
|
-
<p class="plan-price">$0</p>
|
|
841
|
-
<ul>
|
|
842
|
-
<li>Memory, MCP, and Stats</li>
|
|
843
|
-
<li>Lock and local backup</li>
|
|
844
|
-
<li>Hashing embedder — no download</li>
|
|
845
|
-
</ul>
|
|
846
|
-
<button class="btn" type="button" id="continueFree">Continue with free</button>
|
|
847
|
-
</article>
|
|
848
|
-
<article class="plan-card ${tier === "pro" ? "current" : ""}">
|
|
849
|
-
<h2>Pro</h2>
|
|
850
|
-
<p class="plan-price">${esc(proPrice)} <span>once</span></p>
|
|
851
|
-
<ul>
|
|
852
|
-
<li>Everything in Free</li>
|
|
853
|
-
<li>Local n-gram or external embedder</li>
|
|
854
|
-
<li>Hygiene inbox — decay and merge</li>
|
|
855
|
-
<li>Pin facts → Cursor rules</li>
|
|
856
|
-
</ul>
|
|
857
|
-
${
|
|
858
|
-
shopOn
|
|
859
|
-
? `<a class="btn" href="${esc(proUrl)}" target="_blank" rel="noopener">Buy Pro</a>`
|
|
860
|
-
: `<p class="note">Shop is off on this machine.</p>`
|
|
861
|
-
}
|
|
862
|
-
</article>
|
|
863
|
-
<article class="plan-card ${tier === "it" ? "current" : ""}">
|
|
864
|
-
<h2>IT</h2>
|
|
865
|
-
<p class="plan-price">${esc(itPrice)} <span>once</span></p>
|
|
866
|
-
<ul>
|
|
867
|
-
<li>Everything in Pro</li>
|
|
868
|
-
<li>Richer <code>amem doctor --attest</code> (the exclusive)</li>
|
|
869
|
-
<li>Solo agents usually stop at Pro</li>
|
|
870
|
-
</ul>
|
|
871
|
-
${
|
|
872
|
-
shopOn
|
|
873
|
-
? `<a class="btn secondary" href="${esc(itUrl)}" target="_blank" rel="noopener">Buy IT</a>`
|
|
874
|
-
: `<p class="note">Shop is off on this machine.</p>`
|
|
875
|
-
}
|
|
876
|
-
</article>
|
|
877
|
-
</div>
|
|
878
|
-
${licenseApplyHtml("welcome")}
|
|
879
|
-
<p class="note welcome-note">After pay, apply <code>amem-license.json</code> above (or in Setup). Memory never uploads.</p>
|
|
880
|
-
</div>
|
|
881
|
-
</section>`;
|
|
882
|
-
|
|
883
|
-
$("#continueFree")?.addEventListener("click", () => {
|
|
884
|
-
dismissWelcome();
|
|
885
|
-
setTab("setup");
|
|
886
|
-
});
|
|
887
|
-
wireLicenseApply("welcome", async () => {
|
|
888
|
-
await refreshVault();
|
|
889
|
-
if (isPaidLicense()) {
|
|
890
|
-
dismissWelcome();
|
|
891
|
-
setTab("setup");
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
renderWelcome();
|
|
895
|
-
});
|
|
797
|
+
dismissWelcome();
|
|
798
|
+
setTab("dashboard");
|
|
896
799
|
}
|
|
897
800
|
|
|
898
801
|
const SETUP_STEPS = [
|
|
@@ -1051,18 +954,14 @@ function workspaceBlockHtml(ctx) {
|
|
|
1051
954
|
}
|
|
1052
955
|
|
|
1053
956
|
function isPaidLicense() {
|
|
1054
|
-
|
|
1055
|
-
const paid = state.license?.valid !== false && (tier === "pro" || tier === "it");
|
|
1056
|
-
if (paid) persistLicenseTier(tier);
|
|
1057
|
-
paintPlansNavVisibility(paid);
|
|
1058
|
-
return paid;
|
|
957
|
+
return true;
|
|
1059
958
|
}
|
|
1060
959
|
|
|
1061
960
|
/** Paid users don't need the Plans sell tab in the sidebar (Setup still has Apply license). */
|
|
1062
961
|
function paintPlansNavVisibility(paid) {
|
|
1063
962
|
const btn = document.querySelector('#tabs button[data-tab="welcome"]');
|
|
1064
963
|
if (!btn) return;
|
|
1065
|
-
btn.classList.
|
|
964
|
+
btn.classList.add("hidden");
|
|
1066
965
|
}
|
|
1067
966
|
|
|
1068
967
|
function licenseApplyHtml(idPrefix = "lic") {
|
|
@@ -1254,74 +1153,27 @@ function wireProOnboard(idPrefix) {
|
|
|
1254
1153
|
}
|
|
1255
1154
|
|
|
1256
1155
|
function shopBuyCardHtml() {
|
|
1257
|
-
if (state.shop?.enabled === false) return "";
|
|
1258
|
-
const shop = state.shop || {};
|
|
1259
|
-
const proUrl = shop.proUrl || "https://getamem.com/buy/pro";
|
|
1260
|
-
const itUrl = shop.itUrl || "https://getamem.com/buy/it";
|
|
1261
|
-
const proPrice = shop.proPrice || "$12";
|
|
1262
|
-
const itPrice = shop.itPrice || "$49";
|
|
1263
|
-
const tier = String(state.license?.tier || state.status?.license?.tier || "free").toLowerCase();
|
|
1264
|
-
const proOwned = tier === "pro" || tier === "it";
|
|
1265
|
-
const itOwned = tier === "it";
|
|
1266
|
-
if (proOwned) {
|
|
1267
|
-
return `
|
|
1268
|
-
<div class="setup-shop">
|
|
1269
|
-
<div class="setup-shop-head">
|
|
1270
|
-
<div>
|
|
1271
|
-
<h2>${itOwned ? "IT license" : "Pro license"}</h2>
|
|
1272
|
-
<p class="note">Extras unlocked on this machine. ${
|
|
1273
|
-
itOwned
|
|
1274
|
-
? "IT exclusive: richer doctor --attest."
|
|
1275
|
-
: "Upgrade to IT only if you need richer host attestation."
|
|
1276
|
-
}</p>
|
|
1277
|
-
</div>
|
|
1278
|
-
${itOwned ? "" : `<a class="btn secondary small" href="${esc(itUrl)}" target="_blank" rel="noopener">IT · ${esc(itPrice)}</a>`}
|
|
1279
|
-
</div>
|
|
1280
|
-
${proOnboardHtml("setup")}
|
|
1281
|
-
${itPackCardHtml()}
|
|
1282
|
-
</div>`;
|
|
1283
|
-
}
|
|
1284
1156
|
return `
|
|
1285
1157
|
<div class="setup-shop">
|
|
1286
1158
|
<div class="setup-shop-head">
|
|
1287
1159
|
<div>
|
|
1288
|
-
<h2>
|
|
1289
|
-
<p class="note">
|
|
1160
|
+
<h2>All Features Unlocked</h2>
|
|
1161
|
+
<p class="note">amem is 100% free and private. Pro retrieval (n-gram semantic search), memory hygiene, rules sync, security attest, and tasks are all active on this machine.</p>
|
|
1290
1162
|
</div>
|
|
1291
|
-
<button class="btn secondary small" id="seePlans" type="button">What's included</button>
|
|
1292
|
-
</div>
|
|
1293
|
-
<div class="setup-shop-grid">
|
|
1294
|
-
<article class="setup-buy">
|
|
1295
|
-
<div class="setup-buy-top"><strong>Pro</strong></div>
|
|
1296
|
-
<p class="setup-buy-price">${esc(proPrice)} <span>once</span></p>
|
|
1297
|
-
<p class="note">Better local ranking + cleanup + rules sync.</p>
|
|
1298
|
-
<a class="btn" href="${esc(proUrl)}" target="_blank" rel="noopener">Buy Pro</a>
|
|
1299
|
-
</article>
|
|
1300
|
-
<article class="setup-buy">
|
|
1301
|
-
<div class="setup-buy-top"><strong>IT</strong></div>
|
|
1302
|
-
<p class="setup-buy-price">${esc(itPrice)} <span>once</span></p>
|
|
1303
|
-
<p class="note">Pro + richer attest packet for security tickets.</p>
|
|
1304
|
-
<a class="btn secondary" href="${esc(itUrl)}" target="_blank" rel="noopener">Buy IT</a>
|
|
1305
|
-
</article>
|
|
1306
1163
|
</div>
|
|
1307
|
-
${
|
|
1164
|
+
${proOnboardHtml("setup")}
|
|
1308
1165
|
${itPackCardHtml()}
|
|
1309
1166
|
</div>`;
|
|
1310
1167
|
}
|
|
1311
1168
|
|
|
1312
1169
|
function itPackCardHtml() {
|
|
1313
|
-
const itOwned = String(state.license?.tier || "").toLowerCase() === "it";
|
|
1314
1170
|
return `
|
|
1315
1171
|
<div class="it-pack-card" id="itPackCard">
|
|
1316
|
-
<h2>Security pack (local)</h2>
|
|
1317
|
-
<p class="note">Writes policy, MDM plist, SBOM, and offboard script under <code>~/.amem/it-pack</code>.
|
|
1318
|
-
itOwned
|
|
1319
|
-
? "your IT license stamps richer attest when you run doctor."
|
|
1320
|
-
: "IT’s exclusive is the richer <code>amem doctor --attest</code> packet, not this folder."
|
|
1321
|
-
}</p>
|
|
1172
|
+
<h2>Security & Ops pack (local)</h2>
|
|
1173
|
+
<p class="note">Writes policy, MDM plist, SBOM, and offboard script under <code>~/.amem/it-pack</code>. Full <code>amem doctor --attest</code> packet is included.</p>
|
|
1322
1174
|
<div class="actions">
|
|
1323
1175
|
<button class="btn secondary" type="button" id="genItPack">Generate security pack</button>
|
|
1324
|
-
|
|
1176
|
+
<button class="btn secondary" type="button" id="runAttest">Show attest summary</button>
|
|
1325
1177
|
</div>
|
|
1326
1178
|
<pre class="it-pack-out hidden" id="itPackOut"></pre>
|
|
1327
1179
|
</div>`;
|
|
@@ -1995,14 +1847,7 @@ function brainErrorNote() {
|
|
|
1995
1847
|
return m;
|
|
1996
1848
|
}
|
|
1997
1849
|
|
|
1998
|
-
function showdownColumnHtml(title, claims
|
|
1999
|
-
if (locked) {
|
|
2000
|
-
return `<div class="showdown-col">
|
|
2001
|
-
<h3>${esc(title)}</h3>
|
|
2002
|
-
<p class="note">Pro retrieval needs a license.</p>
|
|
2003
|
-
<a class="btn small" href="${esc(shopUrl || "https://getamem.com/buy/pro")}" target="_blank" rel="noopener">Buy Pro</a>
|
|
2004
|
-
</div>`;
|
|
2005
|
-
}
|
|
1850
|
+
function showdownColumnHtml(title, claims) {
|
|
2006
1851
|
if (!claims?.length) {
|
|
2007
1852
|
return `<div class="showdown-col"><h3>${esc(title)}</h3><p class="note">No ranked hits.</p></div>`;
|
|
2008
1853
|
}
|
|
@@ -2029,18 +1874,15 @@ function paintShowdownResult(data) {
|
|
|
2029
1874
|
el.innerHTML = "";
|
|
2030
1875
|
return;
|
|
2031
1876
|
}
|
|
2032
|
-
const shop = state.shop || {};
|
|
2033
1877
|
const proOnly = data.proOnlyIds?.length || 0;
|
|
2034
1878
|
el.innerHTML = `
|
|
2035
1879
|
<div class="showdown-result-bar">
|
|
2036
|
-
<p class="note">
|
|
2037
|
-
data.proLocked ? " · apply a Pro license to unlock the right column" : ""
|
|
2038
|
-
}</p>
|
|
1880
|
+
<p class="note">Semantic (n-gram) unique hits in top results: <b>${proOnly}</b></p>
|
|
2039
1881
|
<button class="btn secondary small" type="button" id="showdownClear">Clear</button>
|
|
2040
1882
|
</div>
|
|
2041
1883
|
<div class="showdown-grid">
|
|
2042
|
-
${showdownColumnHtml("
|
|
2043
|
-
${showdownColumnHtml("Pro
|
|
1884
|
+
${showdownColumnHtml("Hash (128-dim)", data.free)}
|
|
1885
|
+
${showdownColumnHtml("Pro n-gram (256-dim)", data.pro)}
|
|
2044
1886
|
</div>`;
|
|
2045
1887
|
$("#showdownClear")?.addEventListener("click", () => clearRetrievalShowdown());
|
|
2046
1888
|
}
|
|
@@ -2094,50 +1936,9 @@ function dismissSoftPaywall() {
|
|
|
2094
1936
|
|
|
2095
1937
|
async function loadSoftPaywall() {
|
|
2096
1938
|
const el = $("#softPaywall");
|
|
2097
|
-
if (
|
|
2098
|
-
if (isPaidLicense() || softPaywallDismissed()) {
|
|
1939
|
+
if (el) {
|
|
2099
1940
|
el.classList.add("hidden");
|
|
2100
1941
|
el.innerHTML = "";
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
try {
|
|
2104
|
-
const path = state.brainAll ? "/api/hygiene/preview?scope=all" : "/api/hygiene/preview";
|
|
2105
|
-
const preview = state.brainAll ? await apiUnscoped(path) : await api(path);
|
|
2106
|
-
if (!preview?.softPaywall) {
|
|
2107
|
-
el.classList.add("hidden");
|
|
2108
|
-
el.innerHTML = "";
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
const noise = (preview.staleCount || 0) + (preview.duplicateCount || 0);
|
|
2112
|
-
const sessions = Number(preview.sessionCount || 0);
|
|
2113
|
-
const ratio = Number(preview.sessionRatio || 0);
|
|
2114
|
-
const sessionHeavy = sessions >= 25 && ratio >= 0.55;
|
|
2115
|
-
const shop = state.shop || {};
|
|
2116
|
-
const proUrl = shop.proUrl || "https://getamem.com/buy/pro";
|
|
2117
|
-
el.classList.remove("hidden");
|
|
2118
|
-
const why = sessionHeavy
|
|
2119
|
-
? `${preview.active} facts · <b>${Math.round(ratio * 100)}%</b> are short-lived <code>session</code> kinds (~${sessions}). Free preview → Pro applies Cleanup to archive unused sessions.`
|
|
2120
|
-
: `${preview.active} facts · ~${noise} unused/duplicates → about <b>${preview.afterCleanup}</b> after cleanup. Free still works — this is optional.`;
|
|
2121
|
-
el.innerHTML = `
|
|
2122
|
-
<div class="soft-paywall-inner">
|
|
2123
|
-
<div>
|
|
2124
|
-
<strong>${sessionHeavy ? "Session noise is crowding retrieval" : "Pro can clean this"}</strong>
|
|
2125
|
-
<p class="note" style="margin:0.25rem 0 0">${why}</p>
|
|
2126
|
-
</div>
|
|
2127
|
-
<div class="soft-paywall-actions">
|
|
2128
|
-
<a class="btn small" href="${esc(proUrl)}" target="_blank" rel="noopener">Buy Pro · Apply cleanup</a>
|
|
2129
|
-
<button class="btn secondary small" type="button" id="softPaywallShowdown">Compare retrieval</button>
|
|
2130
|
-
<button class="btn secondary small" type="button" id="softPaywallDismiss">Dismiss</button>
|
|
2131
|
-
</div>
|
|
2132
|
-
</div>`;
|
|
2133
|
-
$("#softPaywallDismiss")?.addEventListener("click", () => dismissSoftPaywall());
|
|
2134
|
-
$("#softPaywallShowdown")?.addEventListener("click", () => {
|
|
2135
|
-
state.showdownOpen = true;
|
|
2136
|
-
renderBrain();
|
|
2137
|
-
});
|
|
2138
|
-
maybeNudgeShowdown();
|
|
2139
|
-
} catch {
|
|
2140
|
-
el.classList.add("hidden");
|
|
2141
1942
|
}
|
|
2142
1943
|
}
|
|
2143
1944
|
|
|
@@ -3322,6 +3123,384 @@ function renderStats() {
|
|
|
3322
3123
|
return renderAnalytics();
|
|
3323
3124
|
}
|
|
3324
3125
|
|
|
3126
|
+
const TASK_COLS = [
|
|
3127
|
+
{ id: "backlog", label: "Backlog" },
|
|
3128
|
+
{ id: "next", label: "Next" },
|
|
3129
|
+
{ id: "doing", label: "Doing" },
|
|
3130
|
+
{ id: "blocked", label: "Blocked" },
|
|
3131
|
+
{ id: "done", label: "Done" },
|
|
3132
|
+
];
|
|
3133
|
+
|
|
3134
|
+
async function refreshTasks() {
|
|
3135
|
+
const scopeQuery = state.brainAll ? "?scope=all&include_done=1" : "?include_done=1";
|
|
3136
|
+
const data = state.brainAll
|
|
3137
|
+
? await apiUnscoped(`/api/tasks${scopeQuery}`)
|
|
3138
|
+
: await api(`/api/tasks${scopeQuery}`);
|
|
3139
|
+
state.tasks = data.tasks || [];
|
|
3140
|
+
state.taskCounts = data.counts || {};
|
|
3141
|
+
return data;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/** Skills are a global library, so this never uses the repo-scoped api() helper. */
|
|
3145
|
+
async function refreshSkills() {
|
|
3146
|
+
const [list, drafts] = await Promise.all([
|
|
3147
|
+
apiUnscoped("/api/skills"),
|
|
3148
|
+
apiUnscoped("/api/skills/drafts"),
|
|
3149
|
+
]);
|
|
3150
|
+
state.skills = list.skills || [];
|
|
3151
|
+
state.skillsDir = list.dir || "";
|
|
3152
|
+
state.skillDrafts = drafts.drafts || [];
|
|
3153
|
+
return list;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
function skillCardHtml(s) {
|
|
3157
|
+
const desc = s.description
|
|
3158
|
+
? `<p class="skill-card-desc">${esc(String(s.description).slice(0, 200))}</p>`
|
|
3159
|
+
: `<p class="skill-card-desc muted">No description — agents rank on this, so add one.</p>`;
|
|
3160
|
+
const tags = (s.tags || [])
|
|
3161
|
+
.slice(0, 4)
|
|
3162
|
+
.map((t) => `<span class="skill-tag">${esc(t)}</span>`)
|
|
3163
|
+
.join("");
|
|
3164
|
+
const uses = s.uses > 0 ? `<span class="skill-uses">used ${s.uses}×</span>` : "";
|
|
3165
|
+
return `
|
|
3166
|
+
<article class="skill-card" data-name="${esc(s.name)}">
|
|
3167
|
+
<header class="skill-card-head">
|
|
3168
|
+
<h3>${esc(s.name)}</h3>
|
|
3169
|
+
${uses}
|
|
3170
|
+
</header>
|
|
3171
|
+
${desc}
|
|
3172
|
+
<div class="skill-card-tags">${tags}</div>
|
|
3173
|
+
<div class="skill-card-actions">
|
|
3174
|
+
<button type="button" class="btn secondary small skill-view">View</button>
|
|
3175
|
+
<button type="button" class="btn secondary small skill-del" title="Delete">✕</button>
|
|
3176
|
+
</div>
|
|
3177
|
+
</article>`;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function skillDraftHtml(d) {
|
|
3181
|
+
const why = (d.reasons || []).length
|
|
3182
|
+
? `<p class="skill-draft-why">${esc(d.reasons.join(" · "))}</p>`
|
|
3183
|
+
: "";
|
|
3184
|
+
const label =
|
|
3185
|
+
d.kind === "revision"
|
|
3186
|
+
? `Revise <strong>${esc(d.target_skill || "")}</strong>`
|
|
3187
|
+
: d.kind === "create"
|
|
3188
|
+
? "Staged for review"
|
|
3189
|
+
: "Suggested";
|
|
3190
|
+
// A suggestion has no body yet: only an agent can write one, so do not offer Approve.
|
|
3191
|
+
const approve = d.has_content
|
|
3192
|
+
? `<button type="button" class="btn small skill-draft-apply">Approve</button>`
|
|
3193
|
+
: "";
|
|
3194
|
+
const hint = d.has_content
|
|
3195
|
+
? ""
|
|
3196
|
+
: `<p class="skill-draft-hint">Ask your agent to write this up — it will see the nudge in its next context packet.</p>`;
|
|
3197
|
+
return `
|
|
3198
|
+
<article class="skill-draft" data-id="${esc(d.id)}">
|
|
3199
|
+
<span class="skill-draft-kind">${label}</span>
|
|
3200
|
+
<h4>${esc(d.title)}</h4>
|
|
3201
|
+
${why}
|
|
3202
|
+
${hint}
|
|
3203
|
+
<div class="skill-draft-actions">
|
|
3204
|
+
${approve}
|
|
3205
|
+
<button type="button" class="btn secondary small skill-draft-dismiss">Dismiss</button>
|
|
3206
|
+
</div>
|
|
3207
|
+
</article>`;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
function renderSkills() {
|
|
3211
|
+
const main = $("#main");
|
|
3212
|
+
setMainMode("page");
|
|
3213
|
+
stopAllViz();
|
|
3214
|
+
|
|
3215
|
+
const skills = state.skills || [];
|
|
3216
|
+
const drafts = state.skillDrafts || [];
|
|
3217
|
+
const draftsHtml = drafts.length
|
|
3218
|
+
? `<section class="skill-drafts">
|
|
3219
|
+
<h2>Pending (${drafts.length})</h2>
|
|
3220
|
+
<p class="note">Procedures amem noticed, or agent writes waiting on approval.</p>
|
|
3221
|
+
<div class="skill-draft-list">${drafts.map(skillDraftHtml).join("")}</div>
|
|
3222
|
+
</section>`
|
|
3223
|
+
: "";
|
|
3224
|
+
|
|
3225
|
+
const listHtml = skills.length
|
|
3226
|
+
? `<div class="skill-grid">${skills.map(skillCardHtml).join("")}</div>`
|
|
3227
|
+
: `<div class="skill-empty">
|
|
3228
|
+
<h2>No skills yet</h2>
|
|
3229
|
+
<p>Skills are procedures agents load only when relevant — longer than a memory fact, and kept out of every prompt until needed.</p>
|
|
3230
|
+
<p class="note">Create one with <code>amem skills new <name></code>, or let an agent save one with <code>amem_skill_save</code> after it solves something worth repeating.</p>
|
|
3231
|
+
</div>`;
|
|
3232
|
+
|
|
3233
|
+
main.innerHTML = `
|
|
3234
|
+
<section class="dash-page">
|
|
3235
|
+
<header class="dash-head">
|
|
3236
|
+
<div>
|
|
3237
|
+
<h1>Skills</h1>
|
|
3238
|
+
<p class="sub">Procedural memory — ${skills.length} skill(s) agents can load on demand. Facts live in Memory.</p>
|
|
3239
|
+
</div>
|
|
3240
|
+
<div class="dash-actions">
|
|
3241
|
+
<button class="btn secondary small" type="button" data-go="brain">Open Memory</button>
|
|
3242
|
+
<button class="btn secondary small" type="button" data-go="tasks">Tasks</button>
|
|
3243
|
+
</div>
|
|
3244
|
+
</header>
|
|
3245
|
+
<p class="note">Stored in <code>${esc(state.skillsDir || "~/.amem/skills")}</code> · agents see names and descriptions only until they load one</p>
|
|
3246
|
+
${draftsHtml}
|
|
3247
|
+
${listHtml}
|
|
3248
|
+
<div class="skill-detail" id="skillDetail" hidden></div>
|
|
3249
|
+
</section>`;
|
|
3250
|
+
|
|
3251
|
+
renderPageInsight();
|
|
3252
|
+
|
|
3253
|
+
main.querySelectorAll("[data-go]").forEach((b) => {
|
|
3254
|
+
b.addEventListener("click", () => {
|
|
3255
|
+
const tab = b.getAttribute("data-go");
|
|
3256
|
+
if (tab) setTab(tab);
|
|
3257
|
+
});
|
|
3258
|
+
});
|
|
3259
|
+
|
|
3260
|
+
main.querySelectorAll(".skill-view").forEach((btn) => {
|
|
3261
|
+
btn.addEventListener("click", async () => {
|
|
3262
|
+
const name = btn.closest(".skill-card")?.getAttribute("data-name");
|
|
3263
|
+
if (!name) return;
|
|
3264
|
+
try {
|
|
3265
|
+
const data = await apiUnscoped(`/api/skills/view?name=${encodeURIComponent(name)}`);
|
|
3266
|
+
const host = $("#skillDetail");
|
|
3267
|
+
host.hidden = false;
|
|
3268
|
+
host.innerHTML = `
|
|
3269
|
+
<header class="skill-detail-head">
|
|
3270
|
+
<h2>${esc(data.name)}</h2>
|
|
3271
|
+
<button type="button" class="btn secondary small" id="skillDetailClose">Close</button>
|
|
3272
|
+
</header>
|
|
3273
|
+
<pre class="skill-detail-body">${esc(data.content || "")}</pre>`;
|
|
3274
|
+
$("#skillDetailClose")?.addEventListener("click", () => {
|
|
3275
|
+
host.hidden = true;
|
|
3276
|
+
host.innerHTML = "";
|
|
3277
|
+
});
|
|
3278
|
+
host.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
3279
|
+
} catch (e) {
|
|
3280
|
+
alert(e.message || e);
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
});
|
|
3284
|
+
|
|
3285
|
+
main.querySelectorAll(".skill-del").forEach((btn) => {
|
|
3286
|
+
btn.addEventListener("click", async () => {
|
|
3287
|
+
const name = btn.closest(".skill-card")?.getAttribute("data-name");
|
|
3288
|
+
if (!name || !confirm(`Delete skill "${name}"? This removes the file from disk.`)) return;
|
|
3289
|
+
try {
|
|
3290
|
+
await apiUnscoped(`/api/skills?name=${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
3291
|
+
await refreshSkills();
|
|
3292
|
+
renderSkills();
|
|
3293
|
+
} catch (e) {
|
|
3294
|
+
alert(e.message || e);
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
});
|
|
3298
|
+
|
|
3299
|
+
const draftAction = async (btn, path) => {
|
|
3300
|
+
const id = btn.closest(".skill-draft")?.getAttribute("data-id");
|
|
3301
|
+
if (!id) return;
|
|
3302
|
+
try {
|
|
3303
|
+
await apiUnscoped(path, { method: "POST", body: JSON.stringify({ id }) });
|
|
3304
|
+
await refreshSkills();
|
|
3305
|
+
renderSkills();
|
|
3306
|
+
} catch (e) {
|
|
3307
|
+
alert(e.message || e);
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
main.querySelectorAll(".skill-draft-apply").forEach((btn) => {
|
|
3311
|
+
btn.addEventListener("click", () => draftAction(btn, "/api/skills/drafts/apply"));
|
|
3312
|
+
});
|
|
3313
|
+
main.querySelectorAll(".skill-draft-dismiss").forEach((btn) => {
|
|
3314
|
+
btn.addEventListener("click", () => draftAction(btn, "/api/skills/drafts/dismiss"));
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
/** Where a task created from this board will be filed. */
|
|
3319
|
+
function currentRepoLabel() {
|
|
3320
|
+
const repo = state.status?.repo || matchBoundRepo();
|
|
3321
|
+
return repo?.repo_name || "this folder";
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function taskCardHtml(t) {
|
|
3325
|
+
const body = t.body ? `<p class="tasks-card-body">${esc(String(t.body).slice(0, 160))}</p>` : "";
|
|
3326
|
+
const opts = TASK_COLS.map(
|
|
3327
|
+
(c) => `<option value="${c.id}" ${c.id === t.status ? "selected" : ""}>${esc(c.label)}</option>`,
|
|
3328
|
+
).join("");
|
|
3329
|
+
// Across memories a bare title is ambiguous — say which memory the task came from.
|
|
3330
|
+
const owner =
|
|
3331
|
+
state.brainAll && t.repo_name
|
|
3332
|
+
? `<span class="tasks-card-repo" title="Memory this task belongs to">${esc(t.repo_name)}</span>`
|
|
3333
|
+
: "";
|
|
3334
|
+
return `
|
|
3335
|
+
<article class="tasks-card" data-id="${esc(t.id)}" draggable="true">
|
|
3336
|
+
${owner}
|
|
3337
|
+
<h3>${esc(t.title)}</h3>
|
|
3338
|
+
${body}
|
|
3339
|
+
<div class="tasks-card-actions">
|
|
3340
|
+
<select class="tasks-status" aria-label="Move status">${opts}</select>
|
|
3341
|
+
<button type="button" class="btn secondary small tasks-del" title="Delete">✕</button>
|
|
3342
|
+
</div>
|
|
3343
|
+
</article>`;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
function renderTasks() {
|
|
3347
|
+
const main = $("#main");
|
|
3348
|
+
setMainMode("page");
|
|
3349
|
+
stopAllViz();
|
|
3350
|
+
renderPageInsight();
|
|
3351
|
+
const tasks = Array.isArray(state.tasks) ? state.tasks : [];
|
|
3352
|
+
const focus = statsFocusLabel();
|
|
3353
|
+
const openCount = Number(state.taskCounts?.open ?? tasks.filter((t) => t.status !== "done").length);
|
|
3354
|
+
const byStatus = Object.fromEntries(TASK_COLS.map((c) => [c.id, []]));
|
|
3355
|
+
for (const t of tasks) {
|
|
3356
|
+
if (byStatus[t.status]) byStatus[t.status].push(t);
|
|
3357
|
+
else byStatus.backlog.push(t);
|
|
3358
|
+
}
|
|
3359
|
+
// Done: keep last 12
|
|
3360
|
+
byStatus.done = byStatus.done.slice(0, 12);
|
|
3361
|
+
|
|
3362
|
+
main.innerHTML = `
|
|
3363
|
+
<section class="dash-page">
|
|
3364
|
+
<header class="dash-head">
|
|
3365
|
+
<div>
|
|
3366
|
+
<h1>Tasks</h1>
|
|
3367
|
+
<p class="sub">Deferred work for <strong>${esc(focus)}</strong> — agents park items here so they do not die in chat. Facts still live in Memory.</p>
|
|
3368
|
+
</div>
|
|
3369
|
+
<div class="dash-actions">
|
|
3370
|
+
<button class="btn secondary small" type="button" id="tasksScope">${
|
|
3371
|
+
state.brainAll ? "Show this folder only" : "Show all memory"
|
|
3372
|
+
}</button>
|
|
3373
|
+
<button class="btn secondary small" type="button" data-go="brain">Open Memory</button>
|
|
3374
|
+
<button class="btn secondary small" type="button" data-go="dashboard">Dashboard</button>
|
|
3375
|
+
</div>
|
|
3376
|
+
</header>
|
|
3377
|
+
<p class="note">${openCount} open · use MCP <code>amem_task_add</code> / <code>amem_task_update</code> or the form below${
|
|
3378
|
+
state.brainAll ? ` · new tasks land in <strong>${esc(currentRepoLabel())}</strong>` : ""
|
|
3379
|
+
}</p>
|
|
3380
|
+
<form class="tasks-add" id="tasksAddForm">
|
|
3381
|
+
<input id="tasksTitle" type="text" placeholder="New task title" maxlength="200" required autocomplete="off"/>
|
|
3382
|
+
<input id="tasksBody" type="text" placeholder="Why / notes (optional)" maxlength="400" autocomplete="off"/>
|
|
3383
|
+
<button class="btn" type="submit">Add to backlog</button>
|
|
3384
|
+
</form>
|
|
3385
|
+
<div class="tasks-board" id="tasksBoard">
|
|
3386
|
+
${TASK_COLS.map((col) => {
|
|
3387
|
+
const list = byStatus[col.id] || [];
|
|
3388
|
+
return `
|
|
3389
|
+
<section class="tasks-col" data-status="${col.id}">
|
|
3390
|
+
<header><h2>${esc(col.label)}</h2><span>${list.length}</span></header>
|
|
3391
|
+
<div class="tasks-col-body">${list.map(taskCardHtml).join("") || `<p class="tasks-empty">Empty</p>`}</div>
|
|
3392
|
+
</section>`;
|
|
3393
|
+
}).join("")}
|
|
3394
|
+
</div>
|
|
3395
|
+
</section>`;
|
|
3396
|
+
|
|
3397
|
+
main.querySelectorAll("[data-go]").forEach((b) => {
|
|
3398
|
+
b.addEventListener("click", () => {
|
|
3399
|
+
const tab = b.getAttribute("data-go");
|
|
3400
|
+
if (tab) setTab(tab);
|
|
3401
|
+
});
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
$("#tasksScope")?.addEventListener("click", async () => {
|
|
3405
|
+
state.brainAll = !state.brainAll;
|
|
3406
|
+
persistBrainAll(state.brainAll);
|
|
3407
|
+
writeUrlState();
|
|
3408
|
+
try {
|
|
3409
|
+
await refreshTasks();
|
|
3410
|
+
} catch (err) {
|
|
3411
|
+
alert(err.message || err);
|
|
3412
|
+
}
|
|
3413
|
+
renderTasks();
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
$("#tasksAddForm")?.addEventListener("submit", async (e) => {
|
|
3417
|
+
e.preventDefault();
|
|
3418
|
+
const title = $("#tasksTitle")?.value?.trim();
|
|
3419
|
+
if (!title) return;
|
|
3420
|
+
const body = $("#tasksBody")?.value?.trim() || "";
|
|
3421
|
+
try {
|
|
3422
|
+
const bound = state.status?.repo || matchBoundRepo();
|
|
3423
|
+
const repoId = bound?.id || state.repoId || undefined;
|
|
3424
|
+
await api("/api/tasks", {
|
|
3425
|
+
method: "POST",
|
|
3426
|
+
repoId,
|
|
3427
|
+
body: JSON.stringify({ title, body, status: "backlog", source: "ui" }),
|
|
3428
|
+
});
|
|
3429
|
+
const titleInput = $("#tasksTitle");
|
|
3430
|
+
const bodyInput = $("#tasksBody");
|
|
3431
|
+
if (titleInput) titleInput.value = "";
|
|
3432
|
+
if (bodyInput) bodyInput.value = "";
|
|
3433
|
+
await refreshTasks();
|
|
3434
|
+
renderTasks();
|
|
3435
|
+
} catch (err) {
|
|
3436
|
+
alert(err.message || err);
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3439
|
+
|
|
3440
|
+
main.querySelectorAll(".tasks-card").forEach((card) => {
|
|
3441
|
+
const id = card.getAttribute("data-id");
|
|
3442
|
+
card.querySelector(".tasks-status")?.addEventListener("change", async (ev) => {
|
|
3443
|
+
const status = ev.target.value;
|
|
3444
|
+
try {
|
|
3445
|
+
const url = state.brainAll ? "/api/tasks?scope=all" : "/api/tasks";
|
|
3446
|
+
await (state.brainAll ? apiUnscoped : api)(url, {
|
|
3447
|
+
method: "PATCH",
|
|
3448
|
+
body: JSON.stringify({ id, status, scope: state.brainAll ? "all" : undefined }),
|
|
3449
|
+
});
|
|
3450
|
+
await refreshTasks();
|
|
3451
|
+
renderTasks();
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
alert(err.message || err);
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
card.querySelector(".tasks-del")?.addEventListener("click", async () => {
|
|
3457
|
+
if (!confirm("Delete this task?")) return;
|
|
3458
|
+
try {
|
|
3459
|
+
const url = state.brainAll
|
|
3460
|
+
? `/api/tasks?scope=all&id=${encodeURIComponent(id)}`
|
|
3461
|
+
: `/api/tasks?id=${encodeURIComponent(id)}`;
|
|
3462
|
+
await (state.brainAll ? apiUnscoped : api)(url, { method: "DELETE" });
|
|
3463
|
+
await refreshTasks();
|
|
3464
|
+
renderTasks();
|
|
3465
|
+
} catch (err) {
|
|
3466
|
+
alert(err.message || err);
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
card.addEventListener("dragstart", (ev) => {
|
|
3470
|
+
ev.dataTransfer.setData("text/task-id", id);
|
|
3471
|
+
ev.dataTransfer.effectAllowed = "move";
|
|
3472
|
+
card.classList.add("dragging");
|
|
3473
|
+
});
|
|
3474
|
+
card.addEventListener("dragend", () => card.classList.remove("dragging"));
|
|
3475
|
+
});
|
|
3476
|
+
|
|
3477
|
+
main.querySelectorAll(".tasks-col").forEach((col) => {
|
|
3478
|
+
const status = col.getAttribute("data-status");
|
|
3479
|
+
col.addEventListener("dragover", (ev) => {
|
|
3480
|
+
ev.preventDefault();
|
|
3481
|
+
col.classList.add("drag-over");
|
|
3482
|
+
});
|
|
3483
|
+
col.addEventListener("dragleave", () => col.classList.remove("drag-over"));
|
|
3484
|
+
col.addEventListener("drop", async (ev) => {
|
|
3485
|
+
ev.preventDefault();
|
|
3486
|
+
col.classList.remove("drag-over");
|
|
3487
|
+
const id = ev.dataTransfer.getData("text/task-id");
|
|
3488
|
+
if (!id || !status) return;
|
|
3489
|
+
try {
|
|
3490
|
+
const url = state.brainAll ? "/api/tasks?scope=all" : "/api/tasks";
|
|
3491
|
+
await (state.brainAll ? apiUnscoped : api)(url, {
|
|
3492
|
+
method: "PATCH",
|
|
3493
|
+
body: JSON.stringify({ id, status, scope: state.brainAll ? "all" : undefined }),
|
|
3494
|
+
});
|
|
3495
|
+
await refreshTasks();
|
|
3496
|
+
renderTasks();
|
|
3497
|
+
} catch (err) {
|
|
3498
|
+
alert(err.message || err);
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3325
3504
|
function renderDashboard() {
|
|
3326
3505
|
const main = $("#main");
|
|
3327
3506
|
setMainMode("page");
|
|
@@ -3355,6 +3534,7 @@ function renderDashboard() {
|
|
|
3355
3534
|
</div>
|
|
3356
3535
|
<div class="dash-actions">
|
|
3357
3536
|
<button class="btn secondary small" type="button" data-go="brain">Open Memory</button>
|
|
3537
|
+
<button class="btn secondary small" type="button" data-go="tasks">Open Tasks</button>
|
|
3358
3538
|
<button class="btn secondary small" type="button" data-go="analytics">Open Analytics</button>
|
|
3359
3539
|
</div>
|
|
3360
3540
|
</header>
|
|
@@ -3806,6 +3986,34 @@ async function render() {
|
|
|
3806
3986
|
]);
|
|
3807
3987
|
if (stale()) return;
|
|
3808
3988
|
renderDashboard();
|
|
3989
|
+
} else if (state.tab === "tasks") {
|
|
3990
|
+
stopAllViz();
|
|
3991
|
+
try {
|
|
3992
|
+
await refreshTasks();
|
|
3993
|
+
} catch (e) {
|
|
3994
|
+
if (!stale()) {
|
|
3995
|
+
setMainMode("page");
|
|
3996
|
+
$("#main").innerHTML = `<section class="hero"><div class="hero-inner"><h1>Tasks</h1><p>${esc(e.message || e)}</p></div></section>`;
|
|
3997
|
+
renderPageInsight();
|
|
3998
|
+
}
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
if (stale()) return;
|
|
4002
|
+
renderTasks();
|
|
4003
|
+
} else if (state.tab === "skills") {
|
|
4004
|
+
stopAllViz();
|
|
4005
|
+
try {
|
|
4006
|
+
await refreshSkills();
|
|
4007
|
+
} catch (e) {
|
|
4008
|
+
if (!stale()) {
|
|
4009
|
+
setMainMode("page");
|
|
4010
|
+
$("#main").innerHTML = `<section class="hero"><div class="hero-inner"><h1>Skills</h1><p>${esc(e.message || e)}</p></div></section>`;
|
|
4011
|
+
renderPageInsight();
|
|
4012
|
+
}
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
if (stale()) return;
|
|
4016
|
+
renderSkills();
|
|
3809
4017
|
} else {
|
|
3810
4018
|
stopAllViz();
|
|
3811
4019
|
if (state.tab === "stats") state.tab = "analytics";
|