@monoes/monomindcli 1.11.11 → 1.11.13
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/.claude/skills/mastermind/architect.md +4 -7
- package/.claude/skills/mastermind/autodev.md +1 -3
- package/.claude/skills/mastermind/idea.md +0 -8
- package/.claude/skills/mastermind/monitor.md +2 -2
- package/README.md +286 -129
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +55 -1
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/ui/collector.mjs +118 -9
- package/dist/src/ui/dashboard.html +1432 -493
- package/dist/src/ui/data/agent-avatars.html +763 -0
- package/dist/src/ui/data/agent-avatars.json +966 -0
- package/dist/src/ui/data/avatars/account-strategist.svg +58 -0
- package/dist/src/ui/data/avatars/accounts-payable.svg +54 -0
- package/dist/src/ui/data/avatars/adaptive-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/adaptive-coordinator2.svg +54 -0
- package/dist/src/ui/data/avatars/ai-citation.svg +57 -0
- package/dist/src/ui/data/avatars/ai-engineer.svg +61 -0
- package/dist/src/ui/data/avatars/analytics-reporter.svg +53 -0
- package/dist/src/ui/data/avatars/api-tester.svg +53 -0
- package/dist/src/ui/data/avatars/architecture.svg +54 -0
- package/dist/src/ui/data/avatars/automation-governance.svg +55 -0
- package/dist/src/ui/data/avatars/backend-dev.svg +53 -0
- package/dist/src/ui/data/avatars/benchmarker.svg +54 -0
- package/dist/src/ui/data/avatars/blockchain-auditor.svg +53 -0
- package/dist/src/ui/data/avatars/byzantine-coord.svg +57 -0
- package/dist/src/ui/data/avatars/case-analyst.svg +57 -0
- package/dist/src/ui/data/avatars/cicd-engineer.svg +55 -0
- package/dist/src/ui/data/avatars/cloud-architect.svg +54 -0
- package/dist/src/ui/data/avatars/code-review-swarm.svg +57 -0
- package/dist/src/ui/data/avatars/coder-v119.svg +57 -0
- package/dist/src/ui/data/avatars/coder.svg +58 -0
- package/dist/src/ui/data/avatars/collective-coord.svg +54 -0
- package/dist/src/ui/data/avatars/compliance-auditor.svg +58 -0
- package/dist/src/ui/data/avatars/consensus-coordinator.svg +54 -0
- package/dist/src/ui/data/avatars/content-creator.svg +54 -0
- package/dist/src/ui/data/avatars/crdt-synchronizer.svg +53 -0
- package/dist/src/ui/data/avatars/cro-specialist.svg +58 -0
- package/dist/src/ui/data/avatars/data-consolidator.svg +54 -0
- package/dist/src/ui/data/avatars/data-engineer.svg +53 -0
- package/dist/src/ui/data/avatars/database-optimizer.svg +61 -0
- package/dist/src/ui/data/avatars/deal-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/defender.svg +53 -0
- package/dist/src/ui/data/avatars/devops-automator.svg +56 -0
- package/dist/src/ui/data/avatars/discovery-coach.svg +54 -0
- package/dist/src/ui/data/avatars/email-marketing.svg +57 -0
- package/dist/src/ui/data/avatars/embedded-firmware.svg +61 -0
- package/dist/src/ui/data/avatars/evidence-collector.svg +57 -0
- package/dist/src/ui/data/avatars/experiment-tracker.svg +53 -0
- package/dist/src/ui/data/avatars/feedback-synthesizer.svg +54 -0
- package/dist/src/ui/data/avatars/finance-tracker.svg +54 -0
- package/dist/src/ui/data/avatars/frontend-developer.svg +54 -0
- package/dist/src/ui/data/avatars/game-audio-engineer.svg +59 -0
- package/dist/src/ui/data/avatars/game-designer.svg +54 -0
- package/dist/src/ui/data/avatars/gossip-coordinator.svg +54 -0
- package/dist/src/ui/data/avatars/hierarchical-coord.svg +54 -0
- package/dist/src/ui/data/avatars/incident-commander.svg +57 -0
- package/dist/src/ui/data/avatars/infrastructure.svg +54 -0
- package/dist/src/ui/data/avatars/input-validator.svg +53 -0
- package/dist/src/ui/data/avatars/ios-developer.svg +54 -0
- package/dist/src/ui/data/avatars/issue-tracker.svg +53 -0
- package/dist/src/ui/data/avatars/judge.svg +55 -0
- package/dist/src/ui/data/avatars/launch-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/legal-compliance.svg +53 -0
- package/dist/src/ui/data/avatars/level-designer.svg +53 -0
- package/dist/src/ui/data/avatars/load-balancer.svg +57 -0
- package/dist/src/ui/data/avatars/mcp-builder.svg +53 -0
- package/dist/src/ui/data/avatars/memory-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/mesh-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/ml-developer.svg +58 -0
- package/dist/src/ui/data/avatars/mobile-app-builder.svg +53 -0
- package/dist/src/ui/data/avatars/mobile-dev.svg +54 -0
- package/dist/src/ui/data/avatars/model-qa.svg +58 -0
- package/dist/src/ui/data/avatars/narrative-designer.svg +58 -0
- package/dist/src/ui/data/avatars/outbound-strategist.svg +55 -0
- package/dist/src/ui/data/avatars/path-validator.svg +54 -0
- package/dist/src/ui/data/avatars/payment-agent.svg +53 -0
- package/dist/src/ui/data/avatars/perf-analyzer.svg +58 -0
- package/dist/src/ui/data/avatars/pipeline-analyst.svg +54 -0
- package/dist/src/ui/data/avatars/planner.svg +55 -0
- package/dist/src/ui/data/avatars/pr-manager.svg +54 -0
- package/dist/src/ui/data/avatars/pricing-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/product-manager.svg +54 -0
- package/dist/src/ui/data/avatars/production-validator.svg +54 -0
- package/dist/src/ui/data/avatars/project-shepherd.svg +54 -0
- package/dist/src/ui/data/avatars/proposal-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/prosecutor.svg +57 -0
- package/dist/src/ui/data/avatars/pseudocode.svg +53 -0
- package/dist/src/ui/data/avatars/queen-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/quorum-manager.svg +53 -0
- package/dist/src/ui/data/avatars/raft-manager.svg +53 -0
- package/dist/src/ui/data/avatars/reality-checker.svg +58 -0
- package/dist/src/ui/data/avatars/recruitment.svg +58 -0
- package/dist/src/ui/data/avatars/refinement.svg +53 -0
- package/dist/src/ui/data/avatars/release-manager.svg +54 -0
- package/dist/src/ui/data/avatars/repo-architect.svg +54 -0
- package/dist/src/ui/data/avatars/researcher.svg +58 -0
- package/dist/src/ui/data/avatars/resource-allocator.svg +53 -0
- package/dist/src/ui/data/avatars/reviewer.svg +53 -0
- package/dist/src/ui/data/avatars/safe-executor.svg +53 -0
- package/dist/src/ui/data/avatars/sales-coach.svg +53 -0
- package/dist/src/ui/data/avatars/sales-engineer.svg +58 -0
- package/dist/src/ui/data/avatars/scout-explorer.svg +58 -0
- package/dist/src/ui/data/avatars/security-architect.svg +54 -0
- package/dist/src/ui/data/avatars/security-auditor.svg +55 -0
- package/dist/src/ui/data/avatars/senior-developer.svg +58 -0
- package/dist/src/ui/data/avatars/senior-pm.svg +58 -0
- package/dist/src/ui/data/avatars/seo-specialist.svg +57 -0
- package/dist/src/ui/data/avatars/social-media.svg +54 -0
- package/dist/src/ui/data/avatars/solidity-engineer.svg +58 -0
- package/dist/src/ui/data/avatars/sparc-coder.svg +58 -0
- package/dist/src/ui/data/avatars/sparc-coord.svg +56 -0
- package/dist/src/ui/data/avatars/specification.svg +57 -0
- package/dist/src/ui/data/avatars/sprint-prioritizer.svg +53 -0
- package/dist/src/ui/data/avatars/sre.svg +54 -0
- package/dist/src/ui/data/avatars/studio-operations.svg +53 -0
- package/dist/src/ui/data/avatars/studio-producer.svg +55 -0
- package/dist/src/ui/data/avatars/support-responder.svg +56 -0
- package/dist/src/ui/data/avatars/system-architect.svg +54 -0
- package/dist/src/ui/data/avatars/task-orchestrator.svg +56 -0
- package/dist/src/ui/data/avatars/technical-artist.svg +53 -0
- package/dist/src/ui/data/avatars/technical-writer.svg +59 -0
- package/dist/src/ui/data/avatars/tester.svg +53 -0
- package/dist/src/ui/data/avatars/threat-detection.svg +61 -0
- package/dist/src/ui/data/avatars/trend-researcher.svg +54 -0
- package/dist/src/ui/data/avatars/trial-director.svg +55 -0
- package/dist/src/ui/data/avatars/unity-architect.svg +54 -0
- package/dist/src/ui/data/avatars/visionos-engineer.svg +57 -0
- package/dist/src/ui/data/avatars/worker-specialist.svg +55 -0
- package/dist/src/ui/data/avatars/workflow-architect.svg +57 -0
- package/dist/src/ui/data/avatars/workflow-automation.svg +54 -0
- package/dist/src/ui/data/avatars/zk-steward.svg +54 -0
- package/dist/src/ui/server.mjs +75 -73
- package/dist/src/update/checker.d.ts.map +1 -1
- package/dist/src/update/checker.js +24 -7
- package/dist/src/update/checker.js.map +1 -1
- package/dist/src/update/index.d.ts.map +1 -1
- package/dist/src/update/index.js +3 -6
- package/dist/src/update/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/dist/src/ui/.monomind/data/pending-insights.jsonl +0 -0
- package/dist/src/ui/.monomind/data/ranked-context.json +0 -5
- package/dist/src/ui/.monomind/loops/mastermind-review-1778664132789.json +0 -16
- package/dist/src/ui/.monomind/sessions/current.json +0 -13
- package/dist/src/ui/.monomind/sessions/session-1776778451399.json +0 -15
|
@@ -63,6 +63,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
63
63
|
#view-title { font-size: 14px; font-weight: 600; color: var(--text-hi); }
|
|
64
64
|
.pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-lo); background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 2px 8px; }
|
|
65
65
|
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: blink 2s ease-in-out infinite; }
|
|
66
|
+
.live-dot.polling { background: oklch(70% 0.18 80); animation: blink 4s ease-in-out infinite; }
|
|
66
67
|
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.35} }
|
|
67
68
|
@media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }
|
|
68
69
|
#tb-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
|
@@ -246,6 +247,10 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
246
247
|
.lp-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.4s ease; }
|
|
247
248
|
.loop-stop-btn { font-size: 11px; padding: 2px 8px; background: none; border: 1px solid var(--border); border-radius: 4px; color: var(--text-lo); cursor: pointer; font-family: var(--sans); }
|
|
248
249
|
.loop-stop-btn:hover { border-color: var(--red); color: var(--red); }
|
|
250
|
+
.loop-status.hil { background: oklch(65% 0.15 60 / 0.15); color: oklch(75% 0.16 60); }
|
|
251
|
+
.loop-type-badge { font-size: 10px; padding: 1px 6px; border-radius: 8px; background: var(--accent-dim); color: var(--accent); font-family: var(--mono); flex-shrink: 0; display: inline-block; margin-right: 4px; }
|
|
252
|
+
.loop-type-badge.tillend { background: oklch(65% 0.15 280 / 0.12); color: oklch(70% 0.18 280); }
|
|
253
|
+
.loop-hil-banner { font-size: 11px; background: oklch(65% 0.15 60 / 0.1); border: 1px solid oklch(65% 0.15 60 / 0.3); border-radius: 4px; padding: 3px 8px; color: oklch(75% 0.16 60); margin-top: 5px; }
|
|
249
254
|
|
|
250
255
|
/* memory */
|
|
251
256
|
.mem-section { margin-bottom: 22px; }
|
|
@@ -594,6 +599,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
594
599
|
.shm-grid { display:grid; grid-template-rows:repeat(7,10px); grid-auto-flow:column; grid-auto-columns:10px; gap:2px; margin-top:6px; }
|
|
595
600
|
.shm-cell { border-radius:2px; background:var(--surface-hi); cursor:pointer; transition:outline 0.1s; }
|
|
596
601
|
.shm-cell:hover { outline:1px solid var(--accent); outline-offset:-1px; }
|
|
602
|
+
.shm-cell.shm-0 { cursor: default; }
|
|
597
603
|
.shm-cell.shm-1 { background:oklch(72% 0.18 75 / 0.22); }
|
|
598
604
|
.shm-cell.shm-2 { background:oklch(72% 0.18 75 / 0.42); }
|
|
599
605
|
.shm-cell.shm-3 { background:oklch(72% 0.18 75 / 0.65); }
|
|
@@ -1229,7 +1235,8 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1229
1235
|
.mg-query-row input, .mg-query-row select, .mg-query-row textarea { background:var(--surface-hi); border:1px solid var(--border); border-radius:4px; color:var(--text-hi); padding:6px 10px; font-size:12px; font-family:var(--sans); outline:none; }
|
|
1230
1236
|
.mg-query-row input:focus, .mg-query-row textarea:focus { border-color:var(--accent); }
|
|
1231
1237
|
.mg-query-row textarea { resize:vertical; min-height:70px; width:100%; }
|
|
1232
|
-
.mg-query-result { margin-top:10px; padding:10px 12px; background:var(--surface-hi); border-radius:4px; font-size:11px; font-family:var(--mono); white-space:pre-wrap; word-break:break-word; color:var(--text-mid); max-height:300px; overflow-y:auto; }
|
|
1238
|
+
.mg-query-result { margin-top:10px; padding:10px 12px; background:var(--surface-hi); border-radius:4px; font-size:11px; font-family:var(--mono); white-space:pre-wrap; word-break:break-word; color:var(--text-mid); max-height:300px; overflow-y:auto; cursor:pointer; }
|
|
1239
|
+
.mg-query-result:hover::after { content:'⎘ copy'; position:sticky; float:right; font-size:10px; color:var(--text-xs); pointer-events:none; }
|
|
1233
1240
|
/* live border glow */
|
|
1234
1241
|
@keyframes live-fade { 0% { box-shadow: 0 0 0 1px oklch(72% 0.18 75 / 0.4); } 100% { box-shadow: none; } }
|
|
1235
1242
|
.live-glow { animation: live-fade 8s ease-out forwards; }
|
|
@@ -1291,10 +1298,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1291
1298
|
</div>
|
|
1292
1299
|
<div id="sb-nav">
|
|
1293
1300
|
<div class="nav-sect">
|
|
1294
|
-
<div class="nav-item active" data-view="now">
|
|
1301
|
+
<div class="nav-item active" data-view="now" title="Now — current live activity feed">
|
|
1295
1302
|
<span class="ico">◉</span><span class="lbl">Now</span>
|
|
1296
1303
|
</div>
|
|
1297
|
-
<div class="nav-item" data-view="projects">
|
|
1304
|
+
<div class="nav-item" data-view="projects" title="Projects — switch between projects">
|
|
1298
1305
|
<span class="ico">⊞</span><span class="lbl">Projects</span>
|
|
1299
1306
|
<span class="bdg" id="bdg-projects">—</span>
|
|
1300
1307
|
</div>
|
|
@@ -1306,33 +1313,40 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1306
1313
|
<div class="nav-proj-name" id="nav-proj-name">—</div>
|
|
1307
1314
|
</div>
|
|
1308
1315
|
<div class="nav-proj-items">
|
|
1309
|
-
<div class="nav-item" data-view="sessions">
|
|
1316
|
+
<div class="nav-item" data-view="sessions" title="Sessions — browse and replay project sessions">
|
|
1310
1317
|
<span class="ico">◫</span><span class="lbl">Sessions</span>
|
|
1311
1318
|
<span class="bdg" id="bdg-sessions">—</span>
|
|
1312
1319
|
</div>
|
|
1313
|
-
<div class="nav-item" data-view="loops">
|
|
1320
|
+
<div class="nav-item" data-view="loops" title="Loops — manage automation loops">
|
|
1314
1321
|
<span class="ico">↺</span><span class="lbl">Loops</span>
|
|
1315
1322
|
<span class="bdg" id="bdg-loops">—</span>
|
|
1316
1323
|
</div>
|
|
1317
|
-
<div class="nav-item" data-view="tokens">
|
|
1324
|
+
<div class="nav-item" data-view="tokens" title="Tokens — token usage and cost breakdown">
|
|
1318
1325
|
<span class="ico">$</span><span class="lbl">Tokens</span>
|
|
1319
1326
|
</div>
|
|
1320
|
-
<div class="nav-item" data-view="memory">
|
|
1327
|
+
<div class="nav-item" data-view="memory" title="Memory — agent memory store">
|
|
1321
1328
|
<span class="ico">◈</span><span class="lbl">Memory</span>
|
|
1322
1329
|
</div>
|
|
1323
|
-
<div class="nav-item" data-view="orgs">
|
|
1330
|
+
<div class="nav-item" data-view="orgs" title="Orgs — manage organizations and agents">
|
|
1324
1331
|
<span class="ico">⬡</span><span class="lbl">Orgs</span>
|
|
1325
1332
|
</div>
|
|
1326
|
-
<div class="nav-item" data-view="monograph">
|
|
1333
|
+
<div class="nav-item" data-view="monograph" title="Monograph — knowledge graph explorer">
|
|
1327
1334
|
<span class="ico">⬡</span><span class="lbl">Monograph</span>
|
|
1328
1335
|
</div>
|
|
1329
1336
|
</div>
|
|
1330
1337
|
</div>
|
|
1331
1338
|
<div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
|
|
1332
1339
|
<div class="nav-sect" style="margin-top:auto;padding-top:8px;">
|
|
1333
|
-
<div class="nav-item" data-view="global">
|
|
1340
|
+
<div class="nav-item" data-view="global" title="Global Feed — activity across all projects">
|
|
1334
1341
|
<span class="ico">⊕</span><span class="lbl">Global Feed</span>
|
|
1335
1342
|
</div>
|
|
1343
|
+
<div class="nav-item" data-view="global-loops" title="Global Loops — loops across all projects">
|
|
1344
|
+
<span class="ico">↺</span><span class="lbl">Global Loops</span>
|
|
1345
|
+
<span class="bdg" id="bdg-global-loops">—</span>
|
|
1346
|
+
</div>
|
|
1347
|
+
<div class="nav-item" data-view="global-tokens" title="Global Tokens — token usage across all projects">
|
|
1348
|
+
<span class="ico">$</span><span class="lbl">Global Tokens</span>
|
|
1349
|
+
</div>
|
|
1336
1350
|
</div>
|
|
1337
1351
|
</div>
|
|
1338
1352
|
<div id="sb-footer">
|
|
@@ -1352,9 +1366,9 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1352
1366
|
<div id="tb-right">
|
|
1353
1367
|
<button class="btn" onclick="openMastermind()" style="color:oklch(65% 0.16 295);border-color:oklch(65% 0.16 295 / 0.4)" title="Mastermind — orgs, skills, loops, metrics">⬡ Mastermind</button>
|
|
1354
1368
|
<button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
|
|
1355
|
-
<button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
1369
|
+
<button class="btn" onclick="openCmdPalette()" title="Open command palette (⌘K)">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
1356
1370
|
<button class="btn" onclick="openShortcutHelp()" title="Keyboard shortcuts (?)">? Help</button>
|
|
1357
|
-
<button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
|
|
1371
|
+
<button class="btn" onclick="refreshCurrent()" title="Refresh current view">↺ Refresh</button>
|
|
1358
1372
|
</div>
|
|
1359
1373
|
</div>
|
|
1360
1374
|
|
|
@@ -1400,13 +1414,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1400
1414
|
<div id="feed-search">
|
|
1401
1415
|
<input id="feed-search-input" type="text" placeholder="Search feed…" oninput="filterFeed(this.value)" onkeydown="if(event.key==='Escape')closeFeedSearch()">
|
|
1402
1416
|
<span id="feed-search-count"></span>
|
|
1403
|
-
<button id="feed-search-close" onclick="closeFeedSearch()">✕</button>
|
|
1417
|
+
<button id="feed-search-close" onclick="closeFeedSearch()" title="Close search">✕</button>
|
|
1404
1418
|
</div>
|
|
1405
1419
|
<div id="sess-ctx">
|
|
1406
|
-
<button class="sctx-back" onclick="switchView('sessions')">← Sessions</button>
|
|
1420
|
+
<button class="sctx-back" onclick="switchView('sessions')" title="Back to sessions list">← Sessions</button>
|
|
1407
1421
|
<span class="sctx-sep">/</span>
|
|
1408
1422
|
<span class="sctx-label" id="sctx-label"></span>
|
|
1409
|
-
<button class="sctx-live" onclick="goLive()">⬤ Go live</button>
|
|
1423
|
+
<button class="sctx-live" onclick="goLive()" title="Switch to live feed mode">⬤ Go live</button>
|
|
1410
1424
|
</div>
|
|
1411
1425
|
<div id="feed-recap"></div>
|
|
1412
1426
|
<div id="replay-bar">
|
|
@@ -1420,10 +1434,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1420
1434
|
<div id="feed-timeline" title="Session tool activity timeline"></div>
|
|
1421
1435
|
<div id="feed-time-filter">
|
|
1422
1436
|
<span class="tf-lbl">Range</span>
|
|
1423
|
-
<button class="tf-btn active" data-tf="all" onclick="setFeedTimeFilter('all')">All</button>
|
|
1424
|
-
<button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
|
|
1425
|
-
<button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
|
|
1426
|
-
<button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
|
|
1437
|
+
<button class="tf-btn active" data-tf="all" title="Show all activity" onclick="setFeedTimeFilter('all')">All</button>
|
|
1438
|
+
<button class="tf-btn" data-tf="1h" title="Show last 1 hour" onclick="setFeedTimeFilter('1h')">1h</button>
|
|
1439
|
+
<button class="tf-btn" data-tf="6h" title="Show last 6 hours" onclick="setFeedTimeFilter('6h')">6h</button>
|
|
1440
|
+
<button class="tf-btn" data-tf="24h" title="Show last 24 hours" onclick="setFeedTimeFilter('24h')">24h</button>
|
|
1427
1441
|
<span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate <kbd>↵</kbd> detail <kbd>/</kbd> find <kbd>G</kbd> live <kbd>A</kbd> ambient <kbd>⌘K</kbd> search</span>
|
|
1428
1442
|
</div>
|
|
1429
1443
|
<div id="weekly-card">
|
|
@@ -1452,7 +1466,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1452
1466
|
<div id="detail-panel">
|
|
1453
1467
|
<div id="detail-head">
|
|
1454
1468
|
<h3 id="detail-title">Detail</h3>
|
|
1455
|
-
<button id="detail-close" onclick="closeDetail()">✕</button>
|
|
1469
|
+
<button id="detail-close" onclick="closeDetail()" title="Close detail panel">✕</button>
|
|
1456
1470
|
</div>
|
|
1457
1471
|
<div id="detail-body"></div>
|
|
1458
1472
|
</div>
|
|
@@ -1545,11 +1559,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1545
1559
|
<table class="lb-table"><thead><tr>
|
|
1546
1560
|
<th class="lb-rank">#</th><th>Session</th><th class="lb-cost">Cost</th><th class="lb-dur">Duration</th>
|
|
1547
1561
|
</tr></thead><tbody id="lb-body"></tbody></table>
|
|
1562
|
+
<div id="lb-overflow" style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right"></div>
|
|
1548
1563
|
</div>
|
|
1549
1564
|
<div id="cost-histogram-panel"></div>
|
|
1550
1565
|
<div id="timeline-panel"><div id="timeline-head">Session Timeline <span style="font-weight:400;font-size:10px;color:var(--text-xs)">— each bar = one session, width = duration, color = cost</span></div><div id="timeline-scroll"></div></div>
|
|
1551
1566
|
<div id="model-donut-panel"></div>
|
|
1552
|
-
<div id="file-pivot-bar"><span class="fpb-label" id="fpb-label"></span><button class="fpb-clear" onclick="clearFilePivot()">✕ Clear filter</button></div>
|
|
1567
|
+
<div id="file-pivot-bar"><span class="fpb-label" id="fpb-label"></span><button class="fpb-clear" title="Clear file pivot filter" onclick="clearFilePivot()">✕ Clear filter</button></div>
|
|
1553
1568
|
<div id="model-mix-panel" style="display:none;margin-bottom:16px">
|
|
1554
1569
|
<div id="model-mix-body"></div>
|
|
1555
1570
|
</div>
|
|
@@ -1566,21 +1581,21 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1566
1581
|
<div id="patterns-body"></div>
|
|
1567
1582
|
</div>
|
|
1568
1583
|
<div id="sess-heatmap" style="margin-bottom:14px;display:none">
|
|
1569
|
-
<div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()">✕ Clear filter</button></div>
|
|
1584
|
+
<div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()" title="Clear date filter">✕ Clear filter</button></div>
|
|
1570
1585
|
<div class="shm-grid" id="shm-grid"></div>
|
|
1571
1586
|
</div>
|
|
1572
1587
|
<div class="period-toggles" id="period-toggles">
|
|
1573
1588
|
<span style="font-size:10px;color:var(--text-xs);align-self:center;text-transform:uppercase;letter-spacing:0.06em">Period:</span>
|
|
1574
|
-
<button class="period-btn active" data-period="day" onclick="setPeriod('day')">Day</button>
|
|
1575
|
-
<button class="period-btn" data-period="week" onclick="setPeriod('week')">Week</button>
|
|
1576
|
-
<button class="period-btn" data-period="month" onclick="setPeriod('month')">Month</button>
|
|
1577
|
-
<button class="period-btn" data-period="all" onclick="setPeriod('all')">All</button>
|
|
1589
|
+
<button class="period-btn active" data-period="day" title="Show today's sessions" onclick="setPeriod('day')">Day</button>
|
|
1590
|
+
<button class="period-btn" data-period="week" title="Show this week's sessions" onclick="setPeriod('week')">Week</button>
|
|
1591
|
+
<button class="period-btn" data-period="month" title="Show this month's sessions" onclick="setPeriod('month')">Month</button>
|
|
1592
|
+
<button class="period-btn" data-period="all" title="Show all sessions" onclick="setPeriod('all')">All</button>
|
|
1578
1593
|
</div>
|
|
1579
1594
|
<div id="bulk-toolbar">
|
|
1580
1595
|
<span class="bulk-count" id="bulk-count">0 selected</span>
|
|
1581
|
-
<button class="bulk-btn" onclick="bulkExport()">⬇ Export</button>
|
|
1582
|
-
<button class="bulk-btn" onclick="bulkBookmark()">☆ Bookmark all</button>
|
|
1583
|
-
<button class="bulk-btn danger" onclick="clearBulkSelection()">✕ Clear</button>
|
|
1596
|
+
<button class="bulk-btn" onclick="bulkExport()" title="Export selected sessions">⬇ Export</button>
|
|
1597
|
+
<button class="bulk-btn" onclick="bulkBookmark()" title="Bookmark all selected sessions">☆ Bookmark all</button>
|
|
1598
|
+
<button class="bulk-btn danger" onclick="clearBulkSelection()" title="Clear selection">✕ Clear</button>
|
|
1584
1599
|
</div>
|
|
1585
1600
|
<div id="sess-filter-wrap">
|
|
1586
1601
|
<input id="sess-filter-input" type="text" placeholder="Filter sessions by prompt…" oninput="filterSessions(this.value)" autocomplete="off">
|
|
@@ -1595,32 +1610,33 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1595
1610
|
<div class="vscroll">
|
|
1596
1611
|
<div class="pg-title">Loops</div>
|
|
1597
1612
|
<div class="pg-sub">Scheduled automation loops</div>
|
|
1598
|
-
<button id="btn-new-loop" onclick="showLoopForm()">+ New Loop</button>
|
|
1613
|
+
<button id="btn-new-loop" onclick="showLoopForm()" title="Create a new automation loop">+ New Loop</button>
|
|
1599
1614
|
<div id="loop-create-form" style="display:none">
|
|
1600
1615
|
<div class="lcf-title">Create Loop</div>
|
|
1601
1616
|
<div class="lcf-row">
|
|
1602
1617
|
<label class="lcf-label">Prompt</label>
|
|
1603
|
-
<textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?"></textarea>
|
|
1618
|
+
<textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?" title="The task or goal to run each loop iteration" spellcheck="false"></textarea>
|
|
1604
1619
|
</div>
|
|
1605
1620
|
<div class="lcf-row">
|
|
1606
1621
|
<label class="lcf-label">Name (optional)</label>
|
|
1607
|
-
<input class="lcf-input" id="lcf-name" type="text" placeholder="My loop">
|
|
1622
|
+
<input class="lcf-input" id="lcf-name" type="text" placeholder="My loop" title="Optional display name for this loop">
|
|
1608
1623
|
</div>
|
|
1609
1624
|
<div class="lcf-row-inline">
|
|
1610
1625
|
<div class="lcf-row">
|
|
1611
1626
|
<label class="lcf-label">Interval</label>
|
|
1612
|
-
<input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h">
|
|
1627
|
+
<input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h" title="How often to run (e.g. 30m, 1h, 2h)">
|
|
1613
1628
|
</div>
|
|
1614
1629
|
<div class="lcf-row">
|
|
1615
1630
|
<label class="lcf-label">Max reps (blank = ∞)</label>
|
|
1616
|
-
<input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1">
|
|
1631
|
+
<input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1" title="Maximum number of iterations before stopping (leave blank for unlimited)">
|
|
1617
1632
|
</div>
|
|
1618
1633
|
</div>
|
|
1619
1634
|
<div class="lcf-actions">
|
|
1620
|
-
<button class="lcf-cancel" onclick="hideLoopForm()">Cancel</button>
|
|
1621
|
-
<button class="lcf-submit" onclick="createLoop()">Create Loop</button>
|
|
1635
|
+
<button class="lcf-cancel" title="Discard and close loop form" onclick="hideLoopForm()">Cancel</button>
|
|
1636
|
+
<button class="lcf-submit" title="Create and start the automation loop" onclick="createLoop()">Create Loop</button>
|
|
1622
1637
|
</div>
|
|
1623
1638
|
</div>
|
|
1639
|
+
<div class="filter-bar" style="margin:8px 0"><input class="filter-input" id="loop-list-filter" type="text" placeholder="Filter loops…" oninput="filterLoopList(this.value)" title="Filter loops by name or prompt"></div>
|
|
1624
1640
|
<div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
|
|
1625
1641
|
</div>
|
|
1626
1642
|
</div>
|
|
@@ -1634,10 +1650,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1634
1650
|
<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:12px 14px">
|
|
1635
1651
|
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--text-xs);margin-bottom:8px">Daily usage (last 14 days)</div>
|
|
1636
1652
|
<div class="tok-periods">
|
|
1637
|
-
<button class="tok-period-btn active" data-period="today" onclick="setTokPeriod(this,'today')">Today</button>
|
|
1638
|
-
<button class="tok-period-btn" data-period="week" onclick="setTokPeriod(this,'week')">Week</button>
|
|
1639
|
-
<button class="tok-period-btn" data-period="30d" onclick="setTokPeriod(this,'30d')">30 Days</button>
|
|
1640
|
-
<button class="tok-period-btn" data-period="month" onclick="setTokPeriod(this,'month')">Month</button>
|
|
1653
|
+
<button class="tok-period-btn active" data-period="today" title="Show today's token usage" onclick="setTokPeriod(this,'today')">Today</button>
|
|
1654
|
+
<button class="tok-period-btn" data-period="week" title="Show this week's token usage" onclick="setTokPeriod(this,'week')">Week</button>
|
|
1655
|
+
<button class="tok-period-btn" data-period="30d" title="Show last 30 days of token usage" onclick="setTokPeriod(this,'30d')">30 Days</button>
|
|
1656
|
+
<button class="tok-period-btn" data-period="month" title="Show this month's token usage" onclick="setTokPeriod(this,'month')">Month</button>
|
|
1641
1657
|
</div>
|
|
1642
1658
|
<canvas id="tok-chart" height="100" style="width:100%;display:block"></canvas>
|
|
1643
1659
|
</div>
|
|
@@ -1651,15 +1667,18 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1651
1667
|
<div class="pg-title">Memory</div>
|
|
1652
1668
|
<div class="pg-sub">Knowledge palace — stored facts, graph, identity</div>
|
|
1653
1669
|
<div class="mem-tab-bar" style="display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:6px">
|
|
1654
|
-
<button class="odt-btn active" data-memtab="memories" onclick="switchMemTab('memories')">Memories</button>
|
|
1655
|
-
<button class="odt-btn" data-memtab="routing" onclick="switchMemTab('routing')">Routing</button>
|
|
1656
|
-
<button class="odt-btn" data-memtab="usage" onclick="switchMemTab('usage')">Usage</button>
|
|
1657
|
-
<button class="odt-btn" data-memtab="adrs" onclick="switchMemTab('adrs')">ADRs</button>
|
|
1658
|
-
<button class="odt-btn" data-memtab="swarm" onclick="switchMemTab('swarm')">Swarm</button>
|
|
1659
|
-
<button class="odt-btn" data-memtab="chunks" onclick="switchMemTab('chunks')">Chunks</button>
|
|
1660
|
-
<button class="odt-btn" data-memtab="agent-graph" onclick="switchMemTab('agent-graph')">Agent Graph</button>
|
|
1670
|
+
<button class="odt-btn active" data-memtab="memories" title="Stored agent memories" onclick="switchMemTab('memories')">Memories</button>
|
|
1671
|
+
<button class="odt-btn" data-memtab="routing" title="Routing decisions and feedback" onclick="switchMemTab('routing')">Routing</button>
|
|
1672
|
+
<button class="odt-btn" data-memtab="usage" title="Memory usage statistics" onclick="switchMemTab('usage')">Usage</button>
|
|
1673
|
+
<button class="odt-btn" data-memtab="adrs" title="Architecture Decision Records" onclick="switchMemTab('adrs')">ADRs</button>
|
|
1674
|
+
<button class="odt-btn" data-memtab="swarm" title="Swarm activity and coordination" onclick="switchMemTab('swarm')">Swarm</button>
|
|
1675
|
+
<button class="odt-btn" data-memtab="chunks" title="Knowledge chunks and embeddings" onclick="switchMemTab('chunks')">Chunks</button>
|
|
1676
|
+
<button class="odt-btn" data-memtab="agent-graph" title="Agent interaction graph" onclick="switchMemTab('agent-graph')">Agent Graph</button>
|
|
1661
1677
|
</div>
|
|
1662
1678
|
<div id="mem-tab-memories">
|
|
1679
|
+
<div class="filter-bar" style="margin-bottom:8px">
|
|
1680
|
+
<input class="filter-input" id="mem-list-filter" type="text" placeholder="Filter memories…" oninput="filterMemList(this.value)" title="Filter memories by name or description">
|
|
1681
|
+
</div>
|
|
1663
1682
|
<div class="mem-split" id="mem-split">
|
|
1664
1683
|
<div class="mem-list-pane" id="mem-list-pane">
|
|
1665
1684
|
<div class="loading-txt" style="padding:16px">Loading…</div>
|
|
@@ -1668,7 +1687,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1668
1687
|
<div style="color:var(--text-lo);font-size:13px;padding:20px 0">Select a memory</div>
|
|
1669
1688
|
</div>
|
|
1670
1689
|
</div>
|
|
1671
|
-
<div style="margin-top:10px"><button class="btn" onclick="openNewMemModal()">+ New Memory</button></div>
|
|
1690
|
+
<div style="margin-top:10px"><button class="btn" title="Create a new memory entry" onclick="openNewMemModal()">+ New Memory</button></div>
|
|
1672
1691
|
</div>
|
|
1673
1692
|
<div id="mem-tab-routing" style="display:none"><div class="loading-txt">Loading…</div></div>
|
|
1674
1693
|
<div id="mem-tab-usage" style="display:none"><div class="loading-txt">Loading…</div></div>
|
|
@@ -1685,7 +1704,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1685
1704
|
<div style="color:var(--text-lo);font-size:13px">Select a swarm run</div>
|
|
1686
1705
|
</div>
|
|
1687
1706
|
</div>
|
|
1688
|
-
<button class="btn" style="margin-top:10px;color:var(--red);border-color:var(--red)" onclick="cleanSwarmData()">⌫ Clean Data</button>
|
|
1707
|
+
<button class="btn" title="Remove stale swarm data from memory" style="margin-top:10px;color:var(--red);border-color:var(--red)" onclick="cleanSwarmData()">⌫ Clean Data</button>
|
|
1689
1708
|
</div>
|
|
1690
1709
|
<div id="mem-tab-chunks" style="display:none">
|
|
1691
1710
|
<div class="chunk-stats-bar" id="chunk-stats-bar">
|
|
@@ -1720,6 +1739,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1720
1739
|
<div id="orgs-list-head">
|
|
1721
1740
|
<div class="orgs-view-title">Orgs <span id="orgs-proj-label" style="font-size:11px;font-weight:400;opacity:0.5;margin-left:6px;"></span></div>
|
|
1722
1741
|
<div class="orgs-view-sub" id="orgs-view-sub">MASTERMIND organizations</div>
|
|
1742
|
+
<div style="margin-top:8px"><input class="filter-input" id="org-list-filter" type="text" placeholder="Filter orgs…" oninput="filterOrgList(this.value)" title="Filter organizations by name or goal" style="width:100%;box-sizing:border-box"></div>
|
|
1723
1743
|
</div>
|
|
1724
1744
|
<div id="orgs-list-scroll">
|
|
1725
1745
|
<div class="loading-txt" id="orgs-list-content">Loading…</div>
|
|
@@ -1740,17 +1760,42 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1740
1760
|
<span class="odh-pill" id="odh-topo">—</span>
|
|
1741
1761
|
<span class="odh-pill" id="odh-roles">0 roles</span>
|
|
1742
1762
|
<div class="odh-right">
|
|
1743
|
-
<button class="btn" id="org-
|
|
1763
|
+
<button class="btn" id="org-copy-btn" onclick="v2ShowCopyOrgDialog()" title="Copy this org to another project">Copy to…</button>
|
|
1764
|
+
<button class="btn" id="org-stop-btn" title="Stop this organization" onclick="v2StopOrg()" style="display:none;color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Stop</button>
|
|
1744
1765
|
</div>
|
|
1745
1766
|
</div>
|
|
1767
|
+
|
|
1768
|
+
<!-- Copy org dialog (overlay within the detail pane) -->
|
|
1769
|
+
<div id="org-copy-dialog" style="display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.55);z-index:200;align-items:center;justify-content:center">
|
|
1770
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px 28px;min-width:320px;max-width:480px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4)">
|
|
1771
|
+
<div style="font-size:13px;font-weight:600;color:var(--text-hi);margin-bottom:4px">Copy org to another project</div>
|
|
1772
|
+
<div style="font-size:11px;color:var(--text-lo);margin-bottom:16px">The org config will be copied into the selected project's <code style="font-family:var(--mono)">.monomind/orgs/</code> directory.</div>
|
|
1773
|
+
<div style="margin-bottom:12px">
|
|
1774
|
+
<label style="font-size:11px;color:var(--text-lo);display:block;margin-bottom:5px">Destination project</label>
|
|
1775
|
+
<select id="org-copy-dest-select" title="Select destination project" onchange="if(this.value!=='__custom__')document.getElementById('org-copy-dest-input').value=this.value" style="width:100%;background:var(--surface-hi);border:1px solid var(--border);border-radius:5px;padding:7px 10px;font-size:12px;color:var(--text-hi);font-family:var(--mono)">
|
|
1776
|
+
<option value="">Loading projects…</option>
|
|
1777
|
+
</select>
|
|
1778
|
+
</div>
|
|
1779
|
+
<div style="margin-bottom:14px">
|
|
1780
|
+
<label style="font-size:11px;color:var(--text-lo);display:block;margin-bottom:5px">Or enter a custom absolute path</label>
|
|
1781
|
+
<input id="org-copy-dest-input" type="text" placeholder="/absolute/path/to/project" style="width:100%;background:var(--surface-hi);border:1px solid var(--border);border-radius:5px;padding:7px 10px;font-size:12px;color:var(--text-hi);font-family:var(--mono);box-sizing:border-box" />
|
|
1782
|
+
</div>
|
|
1783
|
+
<div id="org-copy-feedback" style="font-size:11px;margin-bottom:10px;min-height:16px"></div>
|
|
1784
|
+
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
1785
|
+
<button class="btn" title="Discard and close copy dialog" onclick="v2HideCopyOrgDialog()">Cancel</button>
|
|
1786
|
+
<button class="btn" id="org-copy-confirm-btn" title="Copy org to selected project" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
|
|
1787
|
+
</div>
|
|
1788
|
+
</div>
|
|
1789
|
+
</div>
|
|
1790
|
+
|
|
1746
1791
|
<div id="org-detail-tabs">
|
|
1747
|
-
<button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
|
|
1748
|
-
<button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
|
|
1749
|
-
<button class="odt-btn" data-tab="health" onclick="v2SwitchOrgTab('health')">Health</button>
|
|
1750
|
-
<button class="odt-btn" data-tab="approvals" onclick="v2SwitchOrgTab('approvals')">Approvals</button>
|
|
1751
|
-
<button class="odt-btn" data-tab="budgets" onclick="v2SwitchOrgTab('budgets')">Budgets</button>
|
|
1752
|
-
<button class="odt-btn" data-tab="charts" onclick="v2SwitchOrgTab('charts')">Charts</button>
|
|
1753
|
-
<button class="odt-btn" data-tab="skills" onclick="v2SwitchOrgTab('skills')">Skills</button>
|
|
1792
|
+
<button class="odt-btn active" data-tab="chart" title="Agent topology chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
|
|
1793
|
+
<button class="odt-btn" data-tab="activity" title="Recent org activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
|
|
1794
|
+
<button class="odt-btn" data-tab="health" title="Org health and status checks" onclick="v2SwitchOrgTab('health')">Health</button>
|
|
1795
|
+
<button class="odt-btn" data-tab="approvals" title="Pending approvals" onclick="v2SwitchOrgTab('approvals')">Approvals</button>
|
|
1796
|
+
<button class="odt-btn" data-tab="budgets" title="Token budget allocation" onclick="v2SwitchOrgTab('budgets')">Budgets</button>
|
|
1797
|
+
<button class="odt-btn" data-tab="charts" title="Performance charts" onclick="v2SwitchOrgTab('charts')">Charts</button>
|
|
1798
|
+
<button class="odt-btn" data-tab="skills" title="Available skills for this org" onclick="v2SwitchOrgTab('skills')">Skills</button>
|
|
1754
1799
|
</div>
|
|
1755
1800
|
<div id="org-detail-body">
|
|
1756
1801
|
<div class="odt-pane active" id="odt-chart"></div>
|
|
@@ -1799,13 +1844,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1799
1844
|
<div class="pg-sub">Knowledge graph — dependencies, structure, analysis</div>
|
|
1800
1845
|
<!-- Tab bar -->
|
|
1801
1846
|
<div class="mg-tab-bar">
|
|
1802
|
-
<button class="odt-btn active" data-mgtab="overview" onclick="mgSwitchTab('overview')">Overview</button>
|
|
1803
|
-
<button class="odt-btn" data-mgtab="graph" onclick="mgSwitchTab('graph')">Graph</button>
|
|
1804
|
-
<button class="odt-btn" data-mgtab="analyze" onclick="mgSwitchTab('analyze')">Analyze</button>
|
|
1805
|
-
<button class="odt-btn" data-mgtab="query" onclick="mgSwitchTab('query')">Query</button>
|
|
1806
|
-
<button class="odt-btn" data-mgtab="export" onclick="mgSwitchTab('export')">Export</button>
|
|
1807
|
-
<button class="odt-btn" data-mgtab="report" onclick="mgSwitchTab('report')">Report</button>
|
|
1808
|
-
<button class="odt-btn" data-mgtab="wiki" onclick="mgSwitchTab('wiki')">Wiki</button>
|
|
1847
|
+
<button class="odt-btn active" data-mgtab="overview" title="Graph overview — stats, god nodes, top files" onclick="mgSwitchTab('overview')">Overview</button>
|
|
1848
|
+
<button class="odt-btn" data-mgtab="graph" title="Interactive graph visualization" onclick="mgSwitchTab('graph')">Graph</button>
|
|
1849
|
+
<button class="odt-btn" data-mgtab="analyze" title="Analyze graph structure and communities" onclick="mgSwitchTab('analyze')">Analyze</button>
|
|
1850
|
+
<button class="odt-btn" data-mgtab="query" title="Query the knowledge graph" onclick="mgSwitchTab('query')">Query</button>
|
|
1851
|
+
<button class="odt-btn" data-mgtab="export" title="Export graph in various formats" onclick="mgSwitchTab('export')">Export</button>
|
|
1852
|
+
<button class="odt-btn" data-mgtab="report" title="Generate graph health report" onclick="mgSwitchTab('report')">Report</button>
|
|
1853
|
+
<button class="odt-btn" data-mgtab="wiki" title="Browse nodes as a wiki" onclick="mgSwitchTab('wiki')">Wiki</button>
|
|
1809
1854
|
</div>
|
|
1810
1855
|
|
|
1811
1856
|
<!-- TAB 1: OVERVIEW -->
|
|
@@ -1831,9 +1876,9 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1831
1876
|
<div class="mg-pane" id="mg-tab-graph">
|
|
1832
1877
|
<iframe id="mg-iframe" src="" style="width:100%;height:500px;border:none;border-radius:6px;background:var(--surface);" title="Monograph graph"></iframe>
|
|
1833
1878
|
<div class="mg-controls-row">
|
|
1834
|
-
<button class="btn" id="mg-watch-btn" onclick="mgToggleWatch()">WATCH</button>
|
|
1835
|
-
<button class="btn" id="mg-rebuild-btn" onclick="mgRebuild()">REBUILD</button>
|
|
1836
|
-
<a class="btn" id="mg-open-tab-link" href="#" target="_blank" rel="noopener">OPEN IN TAB</a>
|
|
1879
|
+
<button class="btn" id="mg-watch-btn" title="Watch for graph changes and auto-refresh" onclick="mgToggleWatch()">WATCH</button>
|
|
1880
|
+
<button class="btn" id="mg-rebuild-btn" title="Rebuild the knowledge graph from source" onclick="mgRebuild()">REBUILD</button>
|
|
1881
|
+
<a class="btn" id="mg-open-tab-link" href="#" title="Open monograph in a new browser tab" target="_blank" rel="noopener">OPEN IN TAB</a>
|
|
1837
1882
|
<span class="mg-watch-indicator" id="mg-watch-status">○ IDLE</span>
|
|
1838
1883
|
</div>
|
|
1839
1884
|
</div>
|
|
@@ -1866,30 +1911,30 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1866
1911
|
<div class="mg-query-section">
|
|
1867
1912
|
<div class="mqs-title">Impact analysis</div>
|
|
1868
1913
|
<div class="mg-query-row">
|
|
1869
|
-
<input type="text" id="mg-q-impact-node" placeholder="Node ID / name…" style="flex:1;min-width:140px">
|
|
1870
|
-
<select id="mg-q-impact-dir" style="width:120px">
|
|
1914
|
+
<input type="text" id="mg-q-impact-node" placeholder="Node ID / name…" style="flex:1;min-width:140px" onkeydown="if(event.key==='Enter')mgQueryImpact()">
|
|
1915
|
+
<select id="mg-q-impact-dir" title="Impact direction: upstream, downstream, or both" style="width:120px">
|
|
1871
1916
|
<option value="both">Both</option>
|
|
1872
1917
|
<option value="upstream">Upstream</option>
|
|
1873
1918
|
<option value="downstream">Downstream</option>
|
|
1874
1919
|
</select>
|
|
1875
|
-
<button class="btn" onclick="mgQueryImpact()">Run</button>
|
|
1920
|
+
<button class="btn" title="Run impact analysis for this node" onclick="mgQueryImpact()">Run</button>
|
|
1876
1921
|
</div>
|
|
1877
1922
|
<div id="mg-q-impact-result" class="mg-query-result" style="display:none"></div>
|
|
1878
1923
|
</div>
|
|
1879
1924
|
<div class="mg-query-section">
|
|
1880
1925
|
<div class="mqs-title">Context</div>
|
|
1881
1926
|
<div class="mg-query-row">
|
|
1882
|
-
<input type="text" id="mg-q-ctx-node" placeholder="Node ID / file path…" style="flex:1;min-width:200px">
|
|
1883
|
-
<button class="btn" onclick="mgQueryContext()">Run</button>
|
|
1927
|
+
<input type="text" id="mg-q-ctx-node" placeholder="Node ID / file path…" style="flex:1;min-width:200px" onkeydown="if(event.key==='Enter')mgQueryContext()">
|
|
1928
|
+
<button class="btn" title="Get 360° context for this node" onclick="mgQueryContext()">Run</button>
|
|
1884
1929
|
</div>
|
|
1885
1930
|
<div id="mg-q-ctx-result" class="mg-query-result" style="display:none"></div>
|
|
1886
1931
|
</div>
|
|
1887
1932
|
<div class="mg-query-section">
|
|
1888
1933
|
<div class="mqs-title">Shortest path</div>
|
|
1889
1934
|
<div class="mg-query-row">
|
|
1890
|
-
<input type="text" id="mg-q-path-from" placeholder="From node…" style="flex:1;min-width:120px">
|
|
1891
|
-
<input type="text" id="mg-q-path-to" placeholder="To node…" style="flex:1;min-width:120px">
|
|
1892
|
-
<button class="btn" onclick="mgQueryPath()">Run</button>
|
|
1935
|
+
<input type="text" id="mg-q-path-from" placeholder="From node…" style="flex:1;min-width:120px" onkeydown="if(event.key==='Enter')mgQueryPath()">
|
|
1936
|
+
<input type="text" id="mg-q-path-to" placeholder="To node…" style="flex:1;min-width:120px" onkeydown="if(event.key==='Enter')mgQueryPath()">
|
|
1937
|
+
<button class="btn" title="Find shortest path between these two nodes" onclick="mgQueryPath()">Run</button>
|
|
1893
1938
|
</div>
|
|
1894
1939
|
<div id="mg-q-path-result" class="mg-query-result" style="display:none"></div>
|
|
1895
1940
|
</div>
|
|
@@ -1897,48 +1942,48 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1897
1942
|
<div class="mqs-title">Cypher query</div>
|
|
1898
1943
|
<div class="mg-query-row" style="flex-direction:column;align-items:stretch">
|
|
1899
1944
|
<textarea id="mg-q-cypher" placeholder="MATCH (n) RETURN n.name LIMIT 20"></textarea>
|
|
1900
|
-
<button class="btn" style="align-self:flex-end;margin-top:6px" onclick="mgQueryCypher()">Run</button>
|
|
1945
|
+
<button class="btn" title="Execute Cypher query against the graph" style="align-self:flex-end;margin-top:6px" onclick="mgQueryCypher()">Run</button>
|
|
1901
1946
|
</div>
|
|
1902
1947
|
<div id="mg-q-cypher-result" class="mg-query-result" style="display:none"></div>
|
|
1903
1948
|
</div>
|
|
1904
1949
|
<div class="mg-query-section">
|
|
1905
1950
|
<div class="mqs-title">Ask the graph</div>
|
|
1906
1951
|
<div class="mg-query-row">
|
|
1907
|
-
<input type="text" id="mg-q-ask" placeholder="Question or keyword…" style="flex:1;min-width:180px">
|
|
1908
|
-
<select id="mg-q-ask-mode" style="width:110px">
|
|
1952
|
+
<input type="text" id="mg-q-ask" placeholder="Question or keyword…" style="flex:1;min-width:180px" onkeydown="if(event.key==='Enter')mgQueryAsk()">
|
|
1953
|
+
<select id="mg-q-ask-mode" title="Query mode: search, explain, or neighbors" style="width:110px">
|
|
1909
1954
|
<option value="search">Search</option>
|
|
1910
1955
|
<option value="explain">Explain</option>
|
|
1911
1956
|
<option value="neighbors">Neighbors</option>
|
|
1912
1957
|
</select>
|
|
1913
|
-
<select id="mg-q-ask-budget" style="width:80px">
|
|
1958
|
+
<select id="mg-q-ask-budget" title="Max result count" style="width:80px">
|
|
1914
1959
|
<option value="100">100</option>
|
|
1915
1960
|
<option value="500">500</option>
|
|
1916
1961
|
<option value="1000">1000</option>
|
|
1917
1962
|
</select>
|
|
1918
|
-
<button class="btn" onclick="mgQueryAsk()">Ask</button>
|
|
1963
|
+
<button class="btn" title="Search or explain a node in the graph" onclick="mgQueryAsk()">Ask</button>
|
|
1919
1964
|
</div>
|
|
1920
1965
|
<div id="mg-q-ask-result" class="mg-query-result" style="display:none"></div>
|
|
1921
1966
|
</div>
|
|
1922
1967
|
<div class="mg-query-section">
|
|
1923
1968
|
<div class="mqs-title">Ripple impact <span style="font-size:10px;color:var(--text-xs);font-weight:400">multi-hop cascade</span></div>
|
|
1924
1969
|
<div class="mg-query-row">
|
|
1925
|
-
<input type="text" id="mg-q-ripple-node" placeholder="Node name or file…" style="flex:1;min-width:160px">
|
|
1970
|
+
<input type="text" id="mg-q-ripple-node" placeholder="Node name or file…" style="flex:1;min-width:160px" onkeydown="if(event.key==='Enter')mgQueryRipple()">
|
|
1926
1971
|
<select id="mg-q-ripple-hops" style="width:80px" title="Max hops">
|
|
1927
1972
|
<option value="2">2 hops</option>
|
|
1928
1973
|
<option value="3" selected>3 hops</option>
|
|
1929
1974
|
<option value="4">4 hops</option>
|
|
1930
1975
|
<option value="5">5 hops</option>
|
|
1931
1976
|
</select>
|
|
1932
|
-
<button class="btn" onclick="mgQueryRipple()">Run</button>
|
|
1977
|
+
<button class="btn" title="Trace multi-hop ripple impact from this node" onclick="mgQueryRipple()">Run</button>
|
|
1933
1978
|
</div>
|
|
1934
1979
|
<div id="mg-q-ripple-result" class="mg-query-result" style="display:none"></div>
|
|
1935
1980
|
</div>
|
|
1936
1981
|
<div class="mg-query-section">
|
|
1937
1982
|
<div class="mqs-title">Import chain <span style="font-size:10px;color:var(--text-xs);font-weight:400">all paths A → B</span></div>
|
|
1938
1983
|
<div class="mg-query-row">
|
|
1939
|
-
<input type="text" id="mg-q-chain-from" placeholder="From node…" style="flex:1;min-width:120px">
|
|
1940
|
-
<input type="text" id="mg-q-chain-to" placeholder="To node…" style="flex:1;min-width:120px">
|
|
1941
|
-
<button class="btn" onclick="mgQueryImportChain()">Run</button>
|
|
1984
|
+
<input type="text" id="mg-q-chain-from" placeholder="From node…" style="flex:1;min-width:120px" onkeydown="if(event.key==='Enter')mgQueryImportChain()">
|
|
1985
|
+
<input type="text" id="mg-q-chain-to" placeholder="To node…" style="flex:1;min-width:120px" onkeydown="if(event.key==='Enter')mgQueryImportChain()">
|
|
1986
|
+
<button class="btn" title="Find all import paths from A to B" onclick="mgQueryImportChain()">Run</button>
|
|
1942
1987
|
</div>
|
|
1943
1988
|
<div id="mg-q-chain-result" class="mg-query-result" style="display:none"></div>
|
|
1944
1989
|
</div>
|
|
@@ -1954,7 +1999,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1954
1999
|
<!-- TAB 6: REPORT -->
|
|
1955
2000
|
<div class="mg-pane" id="mg-tab-report">
|
|
1956
2001
|
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
|
|
1957
|
-
<button class="btn" onclick="mgLoadReport()">REFRESH REPORT</button>
|
|
2002
|
+
<button class="btn" title="Reload the latest graph health report" onclick="mgLoadReport()">REFRESH REPORT</button>
|
|
1958
2003
|
</div>
|
|
1959
2004
|
<div id="mg-report-content"><div class="loading-txt">Loading…</div></div>
|
|
1960
2005
|
</div>
|
|
@@ -1968,14 +2013,14 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1968
2013
|
</div>
|
|
1969
2014
|
<div class="mg-filter-pills" id="mg-wiki-pills"></div>
|
|
1970
2015
|
<div class="mg-mode-toggle">
|
|
1971
|
-
<button class="btn" id="mg-wiki-mode-all" onclick="mgWikiMode('all')" style="opacity:1">All</button>
|
|
1972
|
-
<button class="btn" id="mg-wiki-mode-docs" onclick="mgWikiMode('docs')" style="opacity:0.5">Docs</button>
|
|
1973
|
-
<button class="btn" id="mg-wiki-mode-code" onclick="mgWikiMode('code')" style="opacity:0.5">Code</button>
|
|
2016
|
+
<button class="btn" id="mg-wiki-mode-all" title="Show all node types" onclick="mgWikiMode('all')" style="opacity:1">All</button>
|
|
2017
|
+
<button class="btn" id="mg-wiki-mode-docs" title="Show documentation nodes only" onclick="mgWikiMode('docs')" style="opacity:0.5">Docs</button>
|
|
2018
|
+
<button class="btn" id="mg-wiki-mode-code" title="Show code nodes only" onclick="mgWikiMode('code')" style="opacity:0.5">Code</button>
|
|
1974
2019
|
</div>
|
|
1975
2020
|
<div class="mg-query-row" style="margin-bottom:10px">
|
|
1976
2021
|
<input type="text" id="mg-wiki-search" placeholder="Search nodes…" style="flex:1" oninput="mgWikiSearchDebounced(this.value)">
|
|
1977
|
-
<button class="btn" onclick="mgRebuildDocs()" id="mg-build-docs-btn">BUILD DOCS</button>
|
|
1978
|
-
<button class="btn" onclick="mgWikiRefresh()">REFRESH</button>
|
|
2022
|
+
<button class="btn" title="Build documentation from source files" onclick="mgRebuildDocs()" id="mg-build-docs-btn">BUILD DOCS</button>
|
|
2023
|
+
<button class="btn" title="Refresh wiki node list" onclick="mgWikiRefresh()">REFRESH</button>
|
|
1979
2024
|
</div>
|
|
1980
2025
|
<div id="mg-wiki-list"></div>
|
|
1981
2026
|
<div id="mg-wiki-detail" class="mg-detail-panel" style="display:none"></div>
|
|
@@ -1994,6 +2039,25 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1994
2039
|
</div>
|
|
1995
2040
|
</div>
|
|
1996
2041
|
|
|
2042
|
+
<div class="view" id="view-global-loops">
|
|
2043
|
+
<div class="vscroll">
|
|
2044
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
2045
|
+
<div class="pg-title" style="margin-bottom:0">Global Loops</div>
|
|
2046
|
+
<span class="pg-sub" id="gl-sub" style="margin-bottom:0">Loops across all projects</span>
|
|
2047
|
+
</div>
|
|
2048
|
+
<div id="gl-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
2049
|
+
</div>
|
|
2050
|
+
</div>
|
|
2051
|
+
<div class="view" id="view-global-tokens">
|
|
2052
|
+
<div class="vscroll">
|
|
2053
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
2054
|
+
<div class="pg-title" style="margin-bottom:0">Global Tokens</div>
|
|
2055
|
+
<span class="pg-sub" id="gt-sub" style="margin-bottom:0">Token usage across all projects</span>
|
|
2056
|
+
</div>
|
|
2057
|
+
<div id="gt-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
2058
|
+
</div>
|
|
2059
|
+
</div>
|
|
2060
|
+
|
|
1997
2061
|
</div><!-- /view-wrap -->
|
|
1998
2062
|
</div><!-- /main -->
|
|
1999
2063
|
<div id="app-ambient-hint">Press A to exit ambient mode</div>
|
|
@@ -2006,12 +2070,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2006
2070
|
<button id="mm-close" onclick="closeMastermind()" aria-label="Close">✕</button>
|
|
2007
2071
|
</div>
|
|
2008
2072
|
<div id="mm-tabs-bar">
|
|
2009
|
-
<button class="mm-tab-btn active" data-mmtab="orgs" onclick="mmSwitchTab('orgs')">Orgs</button>
|
|
2010
|
-
<button class="mm-tab-btn" data-mmtab="skills" onclick="mmSwitchTab('skills')">Skills</button>
|
|
2011
|
-
<button class="mm-tab-btn" data-mmtab="loops" onclick="mmSwitchTab('loops')">Loops</button>
|
|
2012
|
-
<button class="mm-tab-btn" data-mmtab="createorg" onclick="mmSwitchTab('createorg')">Create Org</button>
|
|
2013
|
-
<button class="mm-tab-btn" data-mmtab="metrics" onclick="mmSwitchTab('metrics')">Metrics</button>
|
|
2014
|
-
<button class="mm-tab-btn" data-mmtab="graph" onclick="mmSwitchTab('graph')">Graph</button>
|
|
2073
|
+
<button class="mm-tab-btn active" data-mmtab="orgs" title="Manage organizations" onclick="mmSwitchTab('orgs')">Orgs</button>
|
|
2074
|
+
<button class="mm-tab-btn" data-mmtab="skills" title="Browse available skills" onclick="mmSwitchTab('skills')">Skills</button>
|
|
2075
|
+
<button class="mm-tab-btn" data-mmtab="loops" title="Active automation loops" onclick="mmSwitchTab('loops')">Loops</button>
|
|
2076
|
+
<button class="mm-tab-btn" data-mmtab="createorg" title="Create a new organization" onclick="mmSwitchTab('createorg')">Create Org</button>
|
|
2077
|
+
<button class="mm-tab-btn" data-mmtab="metrics" title="Performance metrics and stats" onclick="mmSwitchTab('metrics')">Metrics</button>
|
|
2078
|
+
<button class="mm-tab-btn" data-mmtab="graph" title="Knowledge graph explorer" onclick="mmSwitchTab('graph')">Graph</button>
|
|
2015
2079
|
</div>
|
|
2016
2080
|
<div id="mm-body"></div>
|
|
2017
2081
|
</div>
|
|
@@ -2021,10 +2085,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2021
2085
|
<div id="chunk-modal-box">
|
|
2022
2086
|
<div id="chunk-modal-title">Edit Chunk</div>
|
|
2023
2087
|
<div id="chunk-modal-src"></div>
|
|
2024
|
-
<textarea id="chunk-modal-ta" spellcheck="false"></textarea>
|
|
2088
|
+
<textarea id="chunk-modal-ta" placeholder="Chunk content…" spellcheck="false"></textarea>
|
|
2025
2089
|
<div class="chunk-modal-btns">
|
|
2026
|
-
<button class="btn" onclick="closeChunkModal()">Cancel</button>
|
|
2027
|
-
<button class="btn" style="color:var(--accent);border-color:var(--accent)" onclick="saveChunkModal()">Save</button>
|
|
2090
|
+
<button class="btn" title="Discard changes" onclick="closeChunkModal()">Cancel</button>
|
|
2091
|
+
<button class="btn" title="Save chunk changes" style="color:var(--accent);border-color:var(--accent)" onclick="saveChunkModal()">Save</button>
|
|
2028
2092
|
</div>
|
|
2029
2093
|
</div>
|
|
2030
2094
|
</div>
|
|
@@ -2032,10 +2096,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2032
2096
|
<div id="mem-modal" onclick="if(event.target===this)closeMemModal()">
|
|
2033
2097
|
<div id="mem-modal-box">
|
|
2034
2098
|
<div id="mem-modal-title">Edit Memory</div>
|
|
2035
|
-
<textarea id="mem-modal-ta" spellcheck="false"></textarea>
|
|
2099
|
+
<textarea id="mem-modal-ta" placeholder="Memory content (YAML or markdown)…" spellcheck="false"></textarea>
|
|
2036
2100
|
<div class="mem-modal-btns">
|
|
2037
|
-
<button class="btn" onclick="closeMemModal()">Cancel</button>
|
|
2038
|
-
<button class="btn" style="color:var(--accent);border-color:var(--accent)" onclick="saveMemModal()">Save</button>
|
|
2101
|
+
<button class="btn" title="Discard changes" onclick="closeMemModal()">Cancel</button>
|
|
2102
|
+
<button class="btn" title="Save memory entry" style="color:var(--accent);border-color:var(--accent)" onclick="saveMemModal()">Save</button>
|
|
2039
2103
|
</div>
|
|
2040
2104
|
</div>
|
|
2041
2105
|
</div>
|
|
@@ -2046,8 +2110,8 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2046
2110
|
<div id="report-box">
|
|
2047
2111
|
<div class="rp-head">
|
|
2048
2112
|
<span class="rp-title">Report Card</span>
|
|
2049
|
-
<button class="rp-copy-btn" onclick="copyReportCard()">⎘ Copy</button>
|
|
2050
|
-
<button class="rp-close-btn" onclick="closeReportCard()">✕</button>
|
|
2113
|
+
<button class="rp-copy-btn" onclick="copyReportCard()" title="Copy report to clipboard">⎘ Copy</button>
|
|
2114
|
+
<button class="rp-close-btn" onclick="closeReportCard()" title="Close report">✕</button>
|
|
2051
2115
|
</div>
|
|
2052
2116
|
<div id="report-content"><pre id="report-pre"></pre></div>
|
|
2053
2117
|
</div>
|
|
@@ -2056,7 +2120,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2056
2120
|
<!-- shortcut help modal -->
|
|
2057
2121
|
<div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
|
|
2058
2122
|
<div id="shortcut-box">
|
|
2059
|
-
<div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
|
|
2123
|
+
<div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()" title="Close shortcuts help">✕</button></div>
|
|
2060
2124
|
<div class="sk-section">Sessions view</div>
|
|
2061
2125
|
<div class="sk-row"><span class="sk-desc">Navigate rows</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
|
|
2062
2126
|
<div class="sk-row"><span class="sk-desc">Open focused session</span><span class="sk-keys"><kbd>↵</kbd></span></div>
|
|
@@ -2065,12 +2129,17 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2065
2129
|
<div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
|
|
2066
2130
|
<div class="sk-row"><span class="sk-desc">Search in feed</span><span class="sk-keys"><kbd>/</kbd></span></div>
|
|
2067
2131
|
<div class="sk-row"><span class="sk-desc">Jump to live session</span><span class="sk-keys"><kbd>G</kbd></span></div>
|
|
2068
|
-
<div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
|
|
2069
2132
|
<div class="sk-row"><span class="sk-desc">Toggle ambient mode</span><span class="sk-keys"><kbd>A</kbd></span></div>
|
|
2070
2133
|
<div class="sk-section">Global</div>
|
|
2134
|
+
<div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
|
|
2135
|
+
<div class="sk-row"><span class="sk-desc">Open Mastermind overlay</span><span class="sk-keys"><kbd>M</kbd></span></div>
|
|
2071
2136
|
<div class="sk-row"><span class="sk-desc">Command palette</span><span class="sk-keys"><kbd>⌘</kbd><kbd>K</kbd></span></div>
|
|
2072
2137
|
<div class="sk-row"><span class="sk-desc">Close / dismiss</span><span class="sk-keys"><kbd>Esc</kbd></span></div>
|
|
2073
2138
|
<div class="sk-row"><span class="sk-desc">This help</span><span class="sk-keys"><kbd>?</kbd></span></div>
|
|
2139
|
+
<div class="sk-section">View navigation</div>
|
|
2140
|
+
<div class="sk-row"><span class="sk-desc">Now / Sessions / Projects</span><span class="sk-keys"><kbd>1</kbd><kbd>2</kbd><kbd>3</kbd></span></div>
|
|
2141
|
+
<div class="sk-row"><span class="sk-desc">Loops / Tokens / Memory</span><span class="sk-keys"><kbd>4</kbd><kbd>5</kbd><kbd>6</kbd></span></div>
|
|
2142
|
+
<div class="sk-row"><span class="sk-desc">Orgs / Monograph / Global</span><span class="sk-keys"><kbd>7</kbd><kbd>8</kbd><kbd>9</kbd></span></div>
|
|
2074
2143
|
</div>
|
|
2075
2144
|
</div>
|
|
2076
2145
|
|
|
@@ -2087,8 +2156,8 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2087
2156
|
<input class="bm-input" id="bm-monthly" type="number" min="0" step="10" placeholder="e.g. 200">
|
|
2088
2157
|
</div>
|
|
2089
2158
|
<div class="bm-btns">
|
|
2090
|
-
<button class="bm-cancel" onclick="closeBudgetModal()">Cancel</button>
|
|
2091
|
-
<button class="bm-save" onclick="saveBudget()">Save</button>
|
|
2159
|
+
<button class="bm-cancel" title="Discard budget changes" onclick="closeBudgetModal()">Cancel</button>
|
|
2160
|
+
<button class="bm-save" title="Save token budget settings" onclick="saveBudget()">Save</button>
|
|
2092
2161
|
</div>
|
|
2093
2162
|
</div>
|
|
2094
2163
|
</div>
|
|
@@ -2108,13 +2177,13 @@ let userScrolled = false;
|
|
|
2108
2177
|
let selectedEntryId = null;
|
|
2109
2178
|
let allDrawers = [];
|
|
2110
2179
|
let dismissedAlerts = new Set();
|
|
2111
|
-
let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
|
|
2180
|
+
let alertState = { todayCost: 0, monthCost: 0, errorCount: 0, longLoops: [], hilLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
|
|
2112
2181
|
let feedTimeFilter = 'all';
|
|
2113
2182
|
let cmdFocusIdx = 0;
|
|
2114
2183
|
let cmdItems = [];
|
|
2115
2184
|
let liveTailMode = false;
|
|
2116
2185
|
let liveTailTimer = null;
|
|
2117
|
-
let bookmarks = new Set(JSON.parse(localStorage.getItem('mm-bookmarks') || '[]'));
|
|
2186
|
+
let bookmarks = new Set((function(){ try { return JSON.parse(localStorage.getItem('mm-bookmarks') || '[]'); } catch { return []; } })());
|
|
2118
2187
|
let showStarredOnly = false;
|
|
2119
2188
|
|
|
2120
2189
|
// ── nav ────────────────────────────────────────────────────
|
|
@@ -2132,8 +2201,11 @@ function switchView(v) {
|
|
|
2132
2201
|
el.classList.toggle('active', el.dataset.view === v));
|
|
2133
2202
|
document.querySelectorAll('.view').forEach(el =>
|
|
2134
2203
|
el.classList.toggle('active', el.id === 'view-' + v));
|
|
2135
|
-
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed' };
|
|
2136
|
-
|
|
2204
|
+
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed', 'global-loops':'Global Loops', 'global-tokens':'Global Tokens' };
|
|
2205
|
+
const viewLabel = titles[v] || v;
|
|
2206
|
+
document.getElementById('view-title').textContent = viewLabel;
|
|
2207
|
+
const proj = DIR.split('/').filter(Boolean).pop() || '';
|
|
2208
|
+
document.title = proj ? `${viewLabel} — ${proj} | monomind` : `${viewLabel} | monomind`;
|
|
2137
2209
|
// Projects always re-fetches so onclick paths in cards stay current
|
|
2138
2210
|
if (v === 'projects') { renderProjects(); return; }
|
|
2139
2211
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
@@ -2149,6 +2221,8 @@ function renderView(v) {
|
|
|
2149
2221
|
if (v === 'orgs') renderOrgs();
|
|
2150
2222
|
if (v === 'monograph') loadMonograph();
|
|
2151
2223
|
if (v === 'global') renderGlobalFeed();
|
|
2224
|
+
if (v === 'global-loops') renderGlobalLoops();
|
|
2225
|
+
if (v === 'global-tokens') renderGlobalTokens();
|
|
2152
2226
|
}
|
|
2153
2227
|
|
|
2154
2228
|
function refreshCurrent() {
|
|
@@ -2164,7 +2238,9 @@ async function init() {
|
|
|
2164
2238
|
ORIGINAL_DIR = DIR;
|
|
2165
2239
|
gitUser = gu;
|
|
2166
2240
|
document.getElementById('sb-user').textContent = gu.name || gu.email || '—';
|
|
2167
|
-
document.getElementById('sb-path')
|
|
2241
|
+
const sbPath = document.getElementById('sb-path');
|
|
2242
|
+
sbPath.textContent = shortPath(DIR);
|
|
2243
|
+
sbPath.title = DIR;
|
|
2168
2244
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
2169
2245
|
_showNavProjectCtx(DIR);
|
|
2170
2246
|
} catch (_) {}
|
|
@@ -2197,7 +2273,9 @@ async function init() {
|
|
|
2197
2273
|
} catch (_) {}
|
|
2198
2274
|
DIR = projParam;
|
|
2199
2275
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
2200
|
-
document.getElementById('sb-path')
|
|
2276
|
+
const sbPath2 = document.getElementById('sb-path');
|
|
2277
|
+
sbPath2.textContent = shortPath(DIR);
|
|
2278
|
+
sbPath2.title = DIR;
|
|
2201
2279
|
_showNavProjectCtx(DIR);
|
|
2202
2280
|
}
|
|
2203
2281
|
restoreURLParams();
|
|
@@ -2208,9 +2286,30 @@ async function init() {
|
|
|
2208
2286
|
initSSE();
|
|
2209
2287
|
}
|
|
2210
2288
|
|
|
2289
|
+
function _setLiveMode(mode) {
|
|
2290
|
+
const dot = document.querySelector('.live-dot');
|
|
2291
|
+
const pill = dot?.closest('.pill');
|
|
2292
|
+
if (!dot) return;
|
|
2293
|
+
if (mode === 'sse') {
|
|
2294
|
+
dot.classList.remove('polling');
|
|
2295
|
+
dot.title = 'Live (SSE)';
|
|
2296
|
+
if (pill) { const t = pill.lastChild; if (t?.nodeType === 3) t.textContent = ' live'; }
|
|
2297
|
+
} else {
|
|
2298
|
+
dot.classList.add('polling');
|
|
2299
|
+
dot.title = 'Polling every 30s (SSE unavailable)';
|
|
2300
|
+
if (pill) { const t = pill.lastChild; if (t?.nodeType === 3) t.textContent = ' polling'; }
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2211
2304
|
function startPolling() {
|
|
2212
2305
|
clearInterval(pollTimer);
|
|
2213
|
-
|
|
2306
|
+
_setLiveMode('poll');
|
|
2307
|
+
pollTimer = setInterval(() => {
|
|
2308
|
+
if (currentView === 'now') refreshNowSilent();
|
|
2309
|
+
else if (currentView === 'loops') renderLoops();
|
|
2310
|
+
else viewRendered['loops'] = false; // loops data may be stale — re-fetch on next nav
|
|
2311
|
+
loadLoopMetrics();
|
|
2312
|
+
}, 30000);
|
|
2214
2313
|
}
|
|
2215
2314
|
|
|
2216
2315
|
let _sseSource = null;
|
|
@@ -2220,16 +2319,17 @@ function initSSE() {
|
|
|
2220
2319
|
try {
|
|
2221
2320
|
const src = new EventSource('/api/events-stream?dir=' + enc(DIR));
|
|
2222
2321
|
src.addEventListener('update', () => { if (currentView === 'now') refreshNowSilent(); });
|
|
2223
|
-
src.addEventListener('connected', () => {});
|
|
2322
|
+
src.addEventListener('connected', () => { _setLiveMode('sse'); });
|
|
2224
2323
|
src.onerror = () => { src.close(); _sseSource = null; startPolling(); };
|
|
2225
2324
|
_sseSource = src;
|
|
2226
2325
|
clearInterval(pollTimer); // SSE replaces polling
|
|
2326
|
+
_setLiveMode('sse');
|
|
2227
2327
|
} catch { startPolling(); }
|
|
2228
2328
|
}
|
|
2229
2329
|
|
|
2230
2330
|
async function apiFetch(url) {
|
|
2231
2331
|
const r = await fetch(url);
|
|
2232
|
-
if (!r.ok) throw new Error(r.status);
|
|
2332
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}${r.statusText ? ' ' + r.statusText : ''}`);
|
|
2233
2333
|
return r.json();
|
|
2234
2334
|
}
|
|
2235
2335
|
|
|
@@ -2262,7 +2362,9 @@ function switchProject(path) {
|
|
|
2262
2362
|
_mgLoaded = false;
|
|
2263
2363
|
_mgGraph = null;
|
|
2264
2364
|
document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
|
|
2265
|
-
document.getElementById('sb-path')
|
|
2365
|
+
const sbPath3 = document.getElementById('sb-path');
|
|
2366
|
+
sbPath3.textContent = shortPath(path);
|
|
2367
|
+
sbPath3.title = path;
|
|
2266
2368
|
_showNavProjectCtx(path);
|
|
2267
2369
|
viewRendered = {};
|
|
2268
2370
|
allSessions = [];
|
|
@@ -2320,7 +2422,7 @@ async function loadFeed() {
|
|
|
2320
2422
|
allSessions = sessions;
|
|
2321
2423
|
document.getElementById('bdg-sessions').textContent = sessions.length || '—';
|
|
2322
2424
|
if (!sessions.length) {
|
|
2323
|
-
setFeedContent('<div class="feed-empty">No sessions yet in this project.</div>');
|
|
2425
|
+
setFeedContent('<div class="feed-empty">No sessions yet in this project.<br><span style="font-size:11px;opacity:0.7">Start Claude Code inside this project to record a session.</span></div>');
|
|
2324
2426
|
return;
|
|
2325
2427
|
}
|
|
2326
2428
|
sessionIdx = 0;
|
|
@@ -2351,7 +2453,9 @@ async function loadFeedSilent() {
|
|
|
2351
2453
|
|
|
2352
2454
|
async function loadFeedForSession(sess) {
|
|
2353
2455
|
if (!sess) return;
|
|
2354
|
-
document.getElementById('feed-sess')
|
|
2456
|
+
const feedSessEl = document.getElementById('feed-sess');
|
|
2457
|
+
feedSessEl.textContent = sess.id.slice(0, 8) + '…';
|
|
2458
|
+
feedSessEl.title = sess.id;
|
|
2355
2459
|
document.getElementById('btn-prev-sess').style.opacity = sessionIdx < allSessions.length - 1 ? '1' : '0.3';
|
|
2356
2460
|
document.getElementById('btn-next-sess').style.opacity = sessionIdx > 0 ? '1' : '0.3';
|
|
2357
2461
|
showSessCtx(sess);
|
|
@@ -2409,7 +2513,7 @@ function catLabel(c) {
|
|
|
2409
2513
|
|
|
2410
2514
|
function renderFeedEvents(events, silent) {
|
|
2411
2515
|
if (!events.length) {
|
|
2412
|
-
if (!silent) setFeedContent('<div class="feed-empty">No events in this session yet.</div>');
|
|
2516
|
+
if (!silent) setFeedContent('<div class="feed-empty">No events in this session yet.<br><span style="font-size:11px;opacity:0.7">Events appear as Claude Code runs tools and makes edits.</span></div>');
|
|
2413
2517
|
return;
|
|
2414
2518
|
}
|
|
2415
2519
|
|
|
@@ -2426,7 +2530,7 @@ function renderFeedEvents(events, silent) {
|
|
|
2426
2530
|
if (feedTimeFilter !== 'all') {
|
|
2427
2531
|
const ms = { '1h': 3600000, '6h': 21600000, '24h': 86400000 }[feedTimeFilter] || 0;
|
|
2428
2532
|
const cutoff = Date.now() - ms;
|
|
2429
|
-
visible = filtered.filter(ev => !ev.ts ||
|
|
2533
|
+
visible = filtered.filter(ev => !ev.ts || (typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || 0) >= cutoff);
|
|
2430
2534
|
}
|
|
2431
2535
|
|
|
2432
2536
|
// update error alert state
|
|
@@ -2480,7 +2584,7 @@ function renderFeedEvents(events, silent) {
|
|
|
2480
2584
|
|
|
2481
2585
|
function renderGroupRow(g) {
|
|
2482
2586
|
const { ico, catCls } = toolStyle(g.cat, '');
|
|
2483
|
-
const itemsData = JSON.stringify(g.items).replace(/'/g, ''');
|
|
2587
|
+
const itemsData = JSON.stringify(g.items).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, ''');
|
|
2484
2588
|
return `<div class="feed-group" data-items='${itemsData}' onclick="expandGroup(this)">
|
|
2485
2589
|
<div class="feed-ico ${catCls}" style="font-size:9px">${ico}</div>
|
|
2486
2590
|
<span class="fg-label">${g.count} ${esc(g.label)}</span>
|
|
@@ -2499,7 +2603,8 @@ function expandGroup(el) {
|
|
|
2499
2603
|
|
|
2500
2604
|
function renderFeedEntry(ev) {
|
|
2501
2605
|
const ts = ev.ts ? relTime(ev.ts) : '';
|
|
2502
|
-
|
|
2606
|
+
const tsTitle = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '';
|
|
2607
|
+
let lbl = '', detail = '', lblTitle = '', id = ev.id || ev.uuid || '';
|
|
2503
2608
|
let catCls, ico;
|
|
2504
2609
|
|
|
2505
2610
|
if (ev.kind === 'tool') {
|
|
@@ -2509,20 +2614,21 @@ function renderFeedEntry(ev) {
|
|
|
2509
2614
|
} else {
|
|
2510
2615
|
ico = '↵'; catCls = 'cat-user';
|
|
2511
2616
|
const t = (ev.text || '').trim();
|
|
2512
|
-
|
|
2617
|
+
if (t.length > 90) { lbl = esc(t.slice(0, 90) + '…'); lblTitle = esc(t); }
|
|
2618
|
+
else lbl = esc(t);
|
|
2513
2619
|
}
|
|
2514
2620
|
|
|
2515
2621
|
const errClass = ev._errored ? ' errored' : '';
|
|
2516
2622
|
const selClass = selectedEntryId && selectedEntryId === id ? ' selected' : '';
|
|
2517
2623
|
|
|
2518
|
-
const evData = JSON.stringify(ev).replace(/'/g, ''');
|
|
2624
|
+
const evData = JSON.stringify(ev).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, ''');
|
|
2519
2625
|
return `<div class="feed-entry k-${ev.kind}${errClass}${selClass}" data-ev='${evData}' onclick="openDetail(this.dataset.ev)">
|
|
2520
2626
|
<div class="feed-ico ${catCls}">${ico}</div>
|
|
2521
2627
|
<div class="feed-body">
|
|
2522
|
-
<div class="feed-lbl">${lbl}</div>
|
|
2628
|
+
<div class="feed-lbl"${lblTitle ? ` title="${lblTitle}"` : ''}>${lbl}</div>
|
|
2523
2629
|
${detail ? `<div class="feed-detail">${detail}</div>` : ''}
|
|
2524
2630
|
</div>
|
|
2525
|
-
<div class="feed-ts">${ts}</div>
|
|
2631
|
+
<div class="feed-ts"${tsTitle ? ` title="${tsTitle}"` : ''}>${ts}</div>
|
|
2526
2632
|
</div>`;
|
|
2527
2633
|
}
|
|
2528
2634
|
|
|
@@ -2558,20 +2664,23 @@ function openDetail(evJson) {
|
|
|
2558
2664
|
|
|
2559
2665
|
if (ev.kind === 'tool') {
|
|
2560
2666
|
const { catCls } = toolStyle(ev.cat, ev.name);
|
|
2667
|
+
const _toolId = (ev.id || '').toString();
|
|
2668
|
+
const _toolLabel = ev.label || ev.name || '';
|
|
2561
2669
|
title = ev.name || 'Tool';
|
|
2562
2670
|
bodyHtml = `
|
|
2563
2671
|
<div class="d-cat-pill ${catCls}" style="font-size:11px">${esc(ev.cat || 'other')}</div>
|
|
2564
|
-
<div class="d-row"><div class="d-lbl">Label</div><div class="d-val">${esc(
|
|
2672
|
+
<div class="d-row"><div class="d-lbl">Label</div><div class="d-val" style="display:flex;align-items:center;gap:6px"><span>${esc(_toolLabel)}</span>${_toolLabel ? `<button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 5px" title="Copy label" onclick="navigator.clipboard.writeText(${JSON.stringify(_toolLabel).replace(/"/g, '"')}).then(()=>showToast('Copied','Label copied','ok'))">⎘</button>` : ''}</div></div>
|
|
2565
2673
|
${ev.subagent ? `<div class="d-row"><div class="d-lbl">Subagent</div><div class="d-val">${esc(ev.subagent)}</div></div>` : ''}
|
|
2566
2674
|
${ev._errored ? `<div class="d-row"><div class="d-lbl">Status</div><div class="d-val error">Error</div></div>` : ''}
|
|
2567
|
-
<div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).
|
|
2568
|
-
<div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val mono">${esc((
|
|
2675
|
+
<div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '—'}</div></div>
|
|
2676
|
+
<div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val" style="display:flex;align-items:center;gap:6px"><span class="mono" title="${esc(_toolId)}">${esc(_toolId.slice(0, 24))}${_toolId.length > 24 ? '…' : ''}</span>${_toolId ? `<button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 5px" title="Copy tool ID" onclick="navigator.clipboard.writeText(${JSON.stringify(_toolId).replace(/"/g, '"')}).then(()=>showToast('Copied','Tool ID copied','ok'))">⎘</button>` : ''}</div></div>
|
|
2569
2677
|
`;
|
|
2570
2678
|
} else if (ev.kind === 'user') {
|
|
2679
|
+
const _userText = ev.text || '';
|
|
2571
2680
|
title = 'User message';
|
|
2572
2681
|
bodyHtml = `
|
|
2573
|
-
<div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).
|
|
2574
|
-
<div class="d-row"><div class="d-lbl">Message</div><div class="d-val" style="white-space:pre-wrap">${esc(
|
|
2682
|
+
<div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '—'}</div></div>
|
|
2683
|
+
<div class="d-row"><div class="d-lbl">Message</div><div class="d-val"><div style="white-space:pre-wrap;margin-bottom:6px">${esc(_userText)}</div>${_userText ? `<button class="btn" style="font-size:10px;padding:1px 6px" title="Copy message text" onclick="navigator.clipboard.writeText(${JSON.stringify(_userText).replace(/"/g, '"')}).then(()=>showToast('Copied','Message copied','ok'))">⎘ Copy</button>` : ''}</div></div>
|
|
2575
2684
|
`;
|
|
2576
2685
|
}
|
|
2577
2686
|
|
|
@@ -2594,15 +2703,15 @@ function buildSparkline() {
|
|
|
2594
2703
|
for (const s of allSessions) {
|
|
2595
2704
|
const ts = s.lastTs || s.mtime;
|
|
2596
2705
|
if (!ts) continue;
|
|
2597
|
-
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
2706
|
+
const age = now - (typeof ts === 'number' ? ts : Number(ts) || new Date(ts).getTime() || 0);
|
|
2598
2707
|
const idx = DAYS - 1 - Math.floor(age / DAY);
|
|
2599
2708
|
if (idx >= 0 && idx < DAYS) buckets[idx]++;
|
|
2600
2709
|
}
|
|
2601
2710
|
const max = Math.max(...buckets, 1);
|
|
2602
|
-
//
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
const
|
|
2711
|
+
// pad so first column starts on Monday: compute day-of-week for day 0 (83 days ago)
|
|
2712
|
+
const firstDow = new Date(now - (DAYS - 1) * DAY).getDay(); // 0=Sun
|
|
2713
|
+
const startOffset = firstDow === 0 ? 6 : firstDow - 1; // Mon=0 offset
|
|
2714
|
+
const padCells = Array.from({ length: startOffset }, () => '<div class="cal-cell" style="opacity:0"></div>');
|
|
2606
2715
|
const cells = buckets.map((v, i) => {
|
|
2607
2716
|
const isToday = i === DAYS - 1;
|
|
2608
2717
|
const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
|
|
@@ -2611,7 +2720,7 @@ function buildSparkline() {
|
|
|
2611
2720
|
const title = `${label}: ${v} session${v !== 1 ? 's' : ''}`;
|
|
2612
2721
|
return `<div class="cal-cell cal-${level}${isToday ? ' cal-today' : ''}" title="${title}"></div>`;
|
|
2613
2722
|
});
|
|
2614
|
-
return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${cells.join('')}</div></div>`;
|
|
2723
|
+
return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${padCells.join('')}${cells.join('')}</div></div>`;
|
|
2615
2724
|
}
|
|
2616
2725
|
|
|
2617
2726
|
// ── alerts rail ────────────────────────────────────────────
|
|
@@ -2641,6 +2750,10 @@ function updateAlerts() {
|
|
|
2641
2750
|
all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
|
|
2642
2751
|
}
|
|
2643
2752
|
|
|
2753
|
+
for (const l of (alertState.hilLoops || [])) {
|
|
2754
|
+
all.push({ id: 'hil-' + l, cls: 'alert-warn', ico: '⚠', msg: `Loop waiting for response: ${l}`, action: `switchView('loops')` });
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2644
2757
|
const visible = all.filter(a => !dismissedAlerts.has(a.id));
|
|
2645
2758
|
if (!visible.length) {
|
|
2646
2759
|
rail.className = '';
|
|
@@ -2650,7 +2763,7 @@ function updateAlerts() {
|
|
|
2650
2763
|
rail.className = 'has-alerts';
|
|
2651
2764
|
rail.innerHTML = visible.map(a =>
|
|
2652
2765
|
`<div class="alert-item ${a.cls}" data-alert-id="${a.id}"${a.action ? ` onclick="${a.action}" style="cursor:pointer"` : ''}>
|
|
2653
|
-
<span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
|
|
2766
|
+
<span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" title="Dismiss" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
|
|
2654
2767
|
</div>`).join('');
|
|
2655
2768
|
}
|
|
2656
2769
|
|
|
@@ -2682,7 +2795,12 @@ function showSessCtx(sess) {
|
|
|
2682
2795
|
bar.classList.remove('show');
|
|
2683
2796
|
return;
|
|
2684
2797
|
}
|
|
2685
|
-
|
|
2798
|
+
const sCtxAge = sess.lastTs || sess.mtime;
|
|
2799
|
+
const sCtxTime = sCtxAge ? ' · ' + relTime(sCtxAge) : '';
|
|
2800
|
+
const sCtxText = sess.lastPrompt || sess.id.slice(0, 16) + '…';
|
|
2801
|
+
const label = document.getElementById('sctx-label');
|
|
2802
|
+
label.textContent = sCtxText + sCtxTime;
|
|
2803
|
+
label.title = sCtxText + (sCtxAge ? ' · ' + new Date(typeof sCtxAge === 'number' ? sCtxAge : Number(sCtxAge) || sCtxAge).toLocaleString() : '');
|
|
2686
2804
|
bar.classList.add('show');
|
|
2687
2805
|
}
|
|
2688
2806
|
|
|
@@ -2704,6 +2822,7 @@ async function loadTodayMetrics() {
|
|
|
2704
2822
|
const data = await apiFetch('/api/section?name=tokens&dir=' + enc(DIR));
|
|
2705
2823
|
const s = data?.tokens?.summary || {};
|
|
2706
2824
|
alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
|
|
2825
|
+
alertState.monthCost = typeof s.monthCost === 'number' ? s.monthCost : 0;
|
|
2707
2826
|
updateAlerts();
|
|
2708
2827
|
checkBudget();
|
|
2709
2828
|
// topbar cost badge
|
|
@@ -2740,13 +2859,17 @@ async function loadLoopMetrics() {
|
|
|
2740
2859
|
try {
|
|
2741
2860
|
const data = await apiFetch('/api/loops?dir=' + enc(DIR));
|
|
2742
2861
|
const loops = Array.isArray(data) ? data : (data.loops || []);
|
|
2743
|
-
|
|
2862
|
+
const hilCount = loops.filter(l => l.status === 'hil:pending').length;
|
|
2863
|
+
document.getElementById('bdg-loops').textContent = loops.length ? (hilCount ? loops.length + '⚠' : loops.length) : '—';
|
|
2744
2864
|
|
|
2745
2865
|
// alert on loops running > 2h
|
|
2746
2866
|
const TWO_HOURS = 2 * 3600 * 1000;
|
|
2747
2867
|
const now = Date.now();
|
|
2748
2868
|
alertState.longLoops = loops
|
|
2749
|
-
.filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
|
|
2869
|
+
.filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.status !== 'hil:pending' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
|
|
2870
|
+
.map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
|
|
2871
|
+
alertState.hilLoops = loops
|
|
2872
|
+
.filter(l => l.status === 'hil:pending')
|
|
2750
2873
|
.map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
|
|
2751
2874
|
updateAlerts();
|
|
2752
2875
|
|
|
@@ -2755,13 +2878,24 @@ async function loadLoopMetrics() {
|
|
|
2755
2878
|
return;
|
|
2756
2879
|
}
|
|
2757
2880
|
const items = loops.slice(0, 5).map(l => {
|
|
2758
|
-
const
|
|
2881
|
+
const fullName = (l.name || l.prompt || 'loop').split('--')[0].trim();
|
|
2882
|
+
const name = fullName.slice(0, 36);
|
|
2883
|
+
const isHilMini = l.status === 'hil:pending';
|
|
2884
|
+
const isTillendMini = l.type === 'tillend';
|
|
2885
|
+
const intervalMini = fmtInterval(l.interval || l.schedule) || 'running';
|
|
2886
|
+
const repMini = isTillendMini && l.currentRep ? `run ${l.currentRep}${l.maxReps ? '/' + l.maxReps : ''}` : null;
|
|
2887
|
+
const hilDot = isHilMini ? ' <span style="color:oklch(75% 0.16 60);font-size:9px">⚠HIL</span>' : '';
|
|
2888
|
+
const typeDot = isTillendMini ? '<span style="color:oklch(70% 0.18 280);font-size:9px;margin-right:3px">∞</span>' : '';
|
|
2759
2889
|
return `<div class="mini-loop">
|
|
2760
|
-
<div class="ml-name">${esc(name)}</div>
|
|
2761
|
-
<div class="ml-meta"><span class="ml-dot"></span>${esc(
|
|
2890
|
+
<div class="ml-name" title="${esc(fullName)}">${typeDot}${esc(name)}${hilDot}</div>
|
|
2891
|
+
<div class="ml-meta"><span class="ml-dot"></span>${esc(repMini || intervalMini)}</div>
|
|
2762
2892
|
</div>`;
|
|
2763
2893
|
}).join('');
|
|
2764
|
-
|
|
2894
|
+
const overflow = loops.length > 5 ? loops.length - 5 : 0;
|
|
2895
|
+
const overflowNote = overflow > 0
|
|
2896
|
+
? `<div style="font-size:10px;color:var(--text-xs);padding:3px 0 0">+${overflow} more — open Loops tab</div>`
|
|
2897
|
+
: '';
|
|
2898
|
+
document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}${overflowNote}`;
|
|
2765
2899
|
} catch (_) {
|
|
2766
2900
|
document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt">—</div>`;
|
|
2767
2901
|
}
|
|
@@ -2793,16 +2927,16 @@ async function loadStatusStrip() {
|
|
|
2793
2927
|
|
|
2794
2928
|
// HNSW status
|
|
2795
2929
|
const hnswOn = mem.hnsw === true || mem.hnswEnabled === true || mem.hnsw_enabled === true;
|
|
2796
|
-
pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
|
|
2930
|
+
pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}" title="Hierarchical Navigable Small World index — fast approximate nearest-neighbour memory search">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
|
|
2797
2931
|
|
|
2798
2932
|
// Patterns count
|
|
2799
2933
|
if (mem.patterns != null) {
|
|
2800
|
-
pills.push(`<span class="ss-pill">PATTERNS ${Number(mem.patterns).toLocaleString()}</span>`);
|
|
2934
|
+
pills.push(`<span class="ss-pill" title="Learned routing patterns stored in AgentDB">PATTERNS ${Number(mem.patterns).toLocaleString()}</span>`);
|
|
2801
2935
|
}
|
|
2802
2936
|
|
|
2803
2937
|
// Chunks count
|
|
2804
2938
|
if (mem.chunks != null) {
|
|
2805
|
-
pills.push(`<span class="ss-pill">CHUNKS ${Number(mem.chunks).toLocaleString()}</span>`);
|
|
2939
|
+
pills.push(`<span class="ss-pill" title="Knowledge chunks indexed for semantic search">CHUNKS ${Number(mem.chunks).toLocaleString()}</span>`);
|
|
2806
2940
|
}
|
|
2807
2941
|
|
|
2808
2942
|
// Swarm status
|
|
@@ -2834,7 +2968,7 @@ async function loadTokensView() {
|
|
|
2834
2968
|
const rows = Array.isArray(data?.tokens?.rows) ? data.tokens.rows : [];
|
|
2835
2969
|
cards.innerHTML = [
|
|
2836
2970
|
{ label:'Today Cost', val: typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—' },
|
|
2837
|
-
{ label:'Today Calls', val: s.todayCalls
|
|
2971
|
+
{ label:'Today Calls', val: s.todayCalls != null ? Number(s.todayCalls).toLocaleString() : '—' },
|
|
2838
2972
|
{ label:'Month Cost', val: typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—' },
|
|
2839
2973
|
{ label:'Total Tokens', val: s.totalTokens != null ? Number(s.totalTokens).toLocaleString() : '—' },
|
|
2840
2974
|
].map(c => `<div class="tok-card"><div class="tc-label">${esc(c.label)}</div><div class="tc-val">${esc(String(c.val))}</div></div>`).join('');
|
|
@@ -2846,12 +2980,13 @@ async function loadTokensView() {
|
|
|
2846
2980
|
'<th style="padding:4px 8px 4px 0">Session</th><th style="padding:4px 8px">Calls</th><th style="padding:4px 8px">Tokens</th><th style="padding:4px 8px">Cost</th>' +
|
|
2847
2981
|
'</tr></thead><tbody>' +
|
|
2848
2982
|
rows.slice(0, 30).map(r => `<tr style="border-top:1px solid var(--border)">
|
|
2849
|
-
<td style="padding:4px 8px 4px 0;color:var(--text-hi);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(r.session || r.id || '—')}</td>
|
|
2850
|
-
<td style="padding:4px 8px;color:var(--text-lo)">${r.calls
|
|
2983
|
+
<td style="padding:4px 8px 4px 0;color:var(--text-hi);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.session || r.id || '')}">${esc(r.session || r.id || '—')}</td>
|
|
2984
|
+
<td style="padding:4px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>
|
|
2851
2985
|
<td style="padding:4px 8px;color:var(--text-lo)">${r.tokens != null ? Number(r.tokens).toLocaleString() : '—'}</td>
|
|
2852
2986
|
<td style="padding:4px 8px;color:var(--accent)">$${Number(r.cost ?? 0).toFixed(4)}</td>
|
|
2853
2987
|
</tr>`).join('') +
|
|
2854
|
-
'</tbody></table></div>'
|
|
2988
|
+
'</tbody></table></div>' +
|
|
2989
|
+
(rows.length > 30 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:6px;text-align:right">Showing 30 of ${rows.length} sessions</div>` : '');
|
|
2855
2990
|
} else { table.innerHTML = ''; }
|
|
2856
2991
|
markLiveGlow('view-tokens');
|
|
2857
2992
|
} catch (_) {
|
|
@@ -2873,6 +3008,36 @@ function renderTokChart(daily, animated = true) {
|
|
|
2873
3008
|
const max = Math.max(...vals, 0.0001);
|
|
2874
3009
|
const avg = vals.reduce((a, b) => a + b, 0) / (vals.length || 1) || 0.0001;
|
|
2875
3010
|
const bar = Math.max(2, Math.floor((W - (vals.length - 1) * 2) / vals.length));
|
|
3011
|
+
window._tokBarW = bar;
|
|
3012
|
+
|
|
3013
|
+
// attach hover tooltip once
|
|
3014
|
+
if (!canvas._tokTipBound) {
|
|
3015
|
+
canvas._tokTipBound = true;
|
|
3016
|
+
canvas.parentNode.style.position = 'relative';
|
|
3017
|
+
const tip = document.createElement('div');
|
|
3018
|
+
tip.style.cssText = 'position:absolute;pointer-events:none;display:none;font-size:11px;font-family:var(--mono);background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:3px 8px;color:var(--text-hi);z-index:100;white-space:nowrap;box-shadow:0 2px 8px #0004';
|
|
3019
|
+
canvas.parentNode.appendChild(tip);
|
|
3020
|
+
canvas._tokTip = tip;
|
|
3021
|
+
canvas.addEventListener('mousemove', e => {
|
|
3022
|
+
const rect = canvas.getBoundingClientRect();
|
|
3023
|
+
const mx = e.clientX - rect.left;
|
|
3024
|
+
const pitch = (window._tokBarW || 8) + 2;
|
|
3025
|
+
const idx = Math.max(0, Math.min(Math.floor(mx / pitch), (window._tokDaily || []).length - 1));
|
|
3026
|
+
const d = (window._tokDaily || [])[idx];
|
|
3027
|
+
if (d) {
|
|
3028
|
+
const label = d.label || d.date || d.day || ('Day ' + (idx + 1));
|
|
3029
|
+
const cost = Number(d.cost ?? d.value ?? 0);
|
|
3030
|
+
canvas._tokTip.textContent = label + ' — $' + (cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2));
|
|
3031
|
+
canvas._tokTip.style.display = 'block';
|
|
3032
|
+
const tipW = canvas._tokTip.offsetWidth || 120;
|
|
3033
|
+
canvas._tokTip.style.left = Math.min(mx + 10, rect.width - tipW - 4) + 'px';
|
|
3034
|
+
canvas._tokTip.style.top = '4px';
|
|
3035
|
+
} else {
|
|
3036
|
+
canvas._tokTip.style.display = 'none';
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
canvas.addEventListener('mouseleave', () => { canvas._tokTip.style.display = 'none'; });
|
|
3040
|
+
}
|
|
2876
3041
|
const targets = vals.map((v, i) => ({
|
|
2877
3042
|
v, i,
|
|
2878
3043
|
isToday: i === vals.length - 1,
|
|
@@ -2933,7 +3098,7 @@ async function setTokPeriod(btn, period) {
|
|
|
2933
3098
|
if (cards) cards.innerHTML = [
|
|
2934
3099
|
{ label: 'Cost', val: typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2)
|
|
2935
3100
|
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '—' },
|
|
2936
|
-
{ label: 'Calls', val: s.todayCalls ?? s.calls ?? '—' },
|
|
3101
|
+
{ label: 'Calls', val: (s.todayCalls ?? s.calls) != null ? Number(s.todayCalls ?? s.calls).toLocaleString() : '—' },
|
|
2937
3102
|
{ label: 'Tokens', val: s.totalTokens != null ? Number(s.totalTokens).toLocaleString() : '—' },
|
|
2938
3103
|
{ label: 'Models', val: s.modelCount ?? s.models ?? '—' },
|
|
2939
3104
|
].map(c => `<div class="tok-card"><div class="tc-label">${esc(c.label)}</div><div class="tc-val">${esc(String(c.val))}</div></div>`).join('');
|
|
@@ -2945,12 +3110,13 @@ async function setTokPeriod(btn, period) {
|
|
|
2945
3110
|
'<th style="padding:3px 8px">Calls</th><th style="padding:3px 8px">Cost</th></tr></thead><tbody>' +
|
|
2946
3111
|
rows.slice(0, 30).map(r =>
|
|
2947
3112
|
`<tr style="border-top:1px solid var(--border)">` +
|
|
2948
|
-
`<td style="padding:3px 8px 3px 0;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(r.session || r.label || r.id || '—')}</td>` +
|
|
2949
|
-
`<td style="padding:3px 8px;color:var(--text-lo)">${r.calls
|
|
3113
|
+
`<td style="padding:3px 8px 3px 0;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.session || r.label || r.id || '')}">${esc(r.session || r.label || r.id || '—')}</td>` +
|
|
3114
|
+
`<td style="padding:3px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>` +
|
|
2950
3115
|
`<td style="padding:3px 8px;color:var(--accent)">$${Number(r.cost ?? 0).toFixed(4)}</td>` +
|
|
2951
3116
|
`</tr>`
|
|
2952
3117
|
).join('') +
|
|
2953
|
-
'</tbody></table></div>'
|
|
3118
|
+
'</tbody></table></div>' +
|
|
3119
|
+
(rows.length > 30 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:6px;text-align:right">Showing 30 of ${rows.length} entries</div>` : '');
|
|
2954
3120
|
} else if (table) { table.innerHTML = ''; }
|
|
2955
3121
|
markLiveGlow('view-tokens');
|
|
2956
3122
|
// Update topbar badge when showing today's data
|
|
@@ -3003,25 +3169,58 @@ async function loadMemRouting() {
|
|
|
3003
3169
|
const last = rows[rows.length - 1];
|
|
3004
3170
|
window._lastRouteAgent = last.suggestedAgent || last.route || last.category || last.agent || last.agentType || '';
|
|
3005
3171
|
}
|
|
3006
|
-
if (!rows.length) { pane.innerHTML = '<div class="empty">No routing data</div>'; return; }
|
|
3007
|
-
|
|
3008
|
-
|
|
3172
|
+
if (!rows.length) { pane.innerHTML = '<div class="empty">No routing data<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Routing data accumulates as agents run tasks. Train the router with <code>npx monomind hooks route --task "…"</code></div></div>'; return; }
|
|
3173
|
+
const displayRows = rows.slice(-40).reverse();
|
|
3174
|
+
const overflowNote = rows.length > 40 ? `<div style="font-size:11px;color:var(--text-xs);margin-bottom:6px;text-align:right">Showing last 40 of ${rows.length} entries</div>` : '';
|
|
3175
|
+
pane.innerHTML = '<div class="filter-bar" style="margin-bottom:8px"><input class="filter-input" id="routing-filter" type="text" placeholder="Filter by agent or task…" oninput="filterRouting(this.value)" title="Filter routing entries"></div>' +
|
|
3176
|
+
overflowNote +
|
|
3177
|
+
'<div id="routing-rows">' +
|
|
3178
|
+
displayRows.map(r => {
|
|
3009
3179
|
const agent = r.suggestedAgent || r.route || r.category || r.agent || '—';
|
|
3010
3180
|
const task = r.task || r.prompt || r.description || r.sessionId?.slice(0, 8) || '—';
|
|
3011
3181
|
const ts = r.timestamp || r.ts || r.created_at;
|
|
3012
3182
|
const conf = r.confidence != null ? Math.round(r.confidence * 100) + '%' : '';
|
|
3013
|
-
return `<div style="padding:5px 0;border-bottom:1px solid var(--border);font-size:11px;font-family:monospace">
|
|
3183
|
+
return `<div class="routing-entry" style="padding:5px 0;border-bottom:1px solid var(--border);font-size:11px;font-family:monospace" data-agent="${esc(agent)}" data-task="${esc(task)}">
|
|
3014
3184
|
<div style="color:var(--text-hi);display:flex;align-items:center;gap:8px">
|
|
3015
3185
|
<span style="color:var(--accent)">${esc(agent)}</span>
|
|
3016
3186
|
${conf ? `<span style="color:var(--text-lo)">${esc(conf)}</span>` : ''}
|
|
3017
|
-
<span style="color:var(--text-lo);margin-left:auto;font-size:10px">${relTime(ts)}</span>
|
|
3187
|
+
<span style="color:var(--text-lo);margin-left:auto;font-size:10px" title="${ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : ''}">${relTime(ts)}</span>
|
|
3018
3188
|
</div>
|
|
3019
3189
|
<div style="color:var(--text-lo);margin-top:1px">${esc(task)}</div>
|
|
3020
3190
|
</div>`;
|
|
3021
|
-
}).join('');
|
|
3191
|
+
}).join('') + '</div>';
|
|
3022
3192
|
} catch (_) { pane.innerHTML = '<div class="empty">Failed to load routing data</div>'; }
|
|
3023
3193
|
}
|
|
3024
3194
|
|
|
3195
|
+
function filterRouting(q) {
|
|
3196
|
+
const lq = (q || '').toLowerCase();
|
|
3197
|
+
document.querySelectorAll('#routing-rows .routing-entry').forEach(el => {
|
|
3198
|
+
const agent = (el.dataset.agent || '').toLowerCase();
|
|
3199
|
+
const task = (el.dataset.task || '').toLowerCase();
|
|
3200
|
+
el.style.display = (!lq || agent.includes(lq) || task.includes(lq)) ? '' : 'none';
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
function filterLoopList(q) {
|
|
3205
|
+
const lq = (q || '').toLowerCase();
|
|
3206
|
+
document.querySelectorAll('#loops-content .loop-row').forEach(el => {
|
|
3207
|
+
const text = (el.textContent || '').toLowerCase();
|
|
3208
|
+
const expand = el.nextElementSibling;
|
|
3209
|
+
const visible = !lq || text.includes(lq);
|
|
3210
|
+
el.style.display = visible ? '' : 'none';
|
|
3211
|
+
if (expand && expand.classList.contains('loop-expand')) expand.style.display = 'none';
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
function filterOrgList(q) {
|
|
3216
|
+
const lq = (q || '').toLowerCase();
|
|
3217
|
+
document.querySelectorAll('#orgs-list-scroll .org-item').forEach(el => {
|
|
3218
|
+
const name = (el.dataset.org || '').toLowerCase();
|
|
3219
|
+
const goal = (el.querySelector('.oi-goal')?.textContent || '').toLowerCase();
|
|
3220
|
+
el.style.display = (!lq || name.includes(lq) || goal.includes(lq)) ? '' : 'none';
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3025
3224
|
async function loadMemUsage() {
|
|
3026
3225
|
const pane = document.getElementById('mem-tab-usage');
|
|
3027
3226
|
if (!pane) return;
|
|
@@ -3029,10 +3228,10 @@ async function loadMemUsage() {
|
|
|
3029
3228
|
// Period tabs
|
|
3030
3229
|
pane.innerHTML = `
|
|
3031
3230
|
<div class="tok-periods" style="margin-bottom:14px">
|
|
3032
|
-
<button class="tok-period-btn active" data-period="today" onclick="loadMemUsagePeriod(this,'today')">Today</button>
|
|
3033
|
-
<button class="tok-period-btn" data-period="week" onclick="loadMemUsagePeriod(this,'week')">Week</button>
|
|
3034
|
-
<button class="tok-period-btn" data-period="30d" onclick="loadMemUsagePeriod(this,'30d')">30 Days</button>
|
|
3035
|
-
<button class="tok-period-btn" data-period="month" onclick="loadMemUsagePeriod(this,'month')">Month</button>
|
|
3231
|
+
<button class="tok-period-btn active" data-period="today" title="Show today's memory usage" onclick="loadMemUsagePeriod(this,'today')">Today</button>
|
|
3232
|
+
<button class="tok-period-btn" data-period="week" title="Show this week's memory usage" onclick="loadMemUsagePeriod(this,'week')">Week</button>
|
|
3233
|
+
<button class="tok-period-btn" data-period="30d" title="Show last 30 days of memory usage" onclick="loadMemUsagePeriod(this,'30d')">30 Days</button>
|
|
3234
|
+
<button class="tok-period-btn" data-period="month" title="Show this month's memory usage" onclick="loadMemUsagePeriod(this,'month')">Month</button>
|
|
3036
3235
|
</div>
|
|
3037
3236
|
<div id="mem-usage-content"><div class="loading-txt">Loading…</div></div>
|
|
3038
3237
|
`;
|
|
@@ -3057,19 +3256,25 @@ async function loadMemUsagePeriod(btn, period) {
|
|
|
3057
3256
|
|
|
3058
3257
|
function barChart(items, valKey, labelKey, color, maxItems) {
|
|
3059
3258
|
if (!items.length) return '<div class="empty" style="font-size:12px">No data</div>';
|
|
3060
|
-
const
|
|
3061
|
-
|
|
3259
|
+
const shown = items.slice(0, maxItems);
|
|
3260
|
+
const maxVal = Math.max(...shown.map(x => Number(x[valKey] || 0)), 0.0001);
|
|
3261
|
+
const rows = shown.map(item => {
|
|
3062
3262
|
const pct = Math.round((Number(item[valKey] || 0) / maxVal) * 100);
|
|
3063
|
-
const
|
|
3263
|
+
const fullLabel = String(item[labelKey] || '—');
|
|
3264
|
+
const label = esc(fullLabel.slice(0, 24));
|
|
3064
3265
|
const val = typeof item[valKey] === 'number' && valKey === 'cost'
|
|
3065
3266
|
? '$' + Number(item[valKey]).toFixed(4)
|
|
3066
3267
|
: String(item[valKey] || 0);
|
|
3067
3268
|
return `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">
|
|
3068
|
-
<div style="width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px">${label}</div>
|
|
3069
|
-
<div style="flex:1;height:7px;background:var(--border);border-radius:2px;overflow:hidden"><div style="width:${pct}%;height:100%;background:${color};border-radius:2px"></div></div>
|
|
3269
|
+
<div style="width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc(fullLabel)}">${label}${fullLabel.length > 24 ? '…' : ''}</div>
|
|
3270
|
+
<div style="flex:1;height:7px;background:var(--border);border-radius:2px;overflow:hidden" title="${esc(fullLabel)}: ${esc(val)}"><div style="width:${pct}%;height:100%;background:${color};border-radius:2px"></div></div>
|
|
3070
3271
|
<div style="width:60px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">${esc(val)}</div>
|
|
3071
3272
|
</div>`;
|
|
3072
3273
|
}).join('');
|
|
3274
|
+
const overflow = items.length > maxItems
|
|
3275
|
+
? `<div style="font-size:11px;color:var(--text-xs);margin-top:3px;text-align:right">Showing ${maxItems} of ${items.length}</div>`
|
|
3276
|
+
: '';
|
|
3277
|
+
return rows + overflow;
|
|
3073
3278
|
}
|
|
3074
3279
|
|
|
3075
3280
|
const totalCost = typeof s.todayCost === 'number' ? s.todayCost : (typeof s.cost === 'number' ? s.cost : null);
|
|
@@ -3112,11 +3317,12 @@ async function loadMemUsagePeriod(btn, period) {
|
|
|
3112
3317
|
<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px">
|
|
3113
3318
|
<thead><tr style="color:var(--text-xs);text-align:left"><th style="padding:3px 8px 3px 0">Session</th><th>Calls</th><th>Cost</th></tr></thead>
|
|
3114
3319
|
<tbody>${rows.slice(0,30).map(r => `<tr style="border-top:1px solid var(--border)">
|
|
3115
|
-
<td style="padding:3px 8px 3px 0;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:11px">${esc(r.session||r.label||r.id||'—')}</td>
|
|
3116
|
-
<td style="padding:3px 8px;color:var(--text-lo)">${r.calls
|
|
3320
|
+
<td style="padding:3px 8px 3px 0;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:11px" title="${esc(r.session||r.label||r.id||'')}">${esc(r.session||r.label||r.id||'—')}</td>
|
|
3321
|
+
<td style="padding:3px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>
|
|
3117
3322
|
<td style="padding:3px 8px;color:var(--accent)">$${Number(r.cost??0).toFixed(4)}</td>
|
|
3118
3323
|
</tr>`).join('')}</tbody>
|
|
3119
|
-
</table></div
|
|
3324
|
+
</table></div>
|
|
3325
|
+
${rows.length > 30 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:6px;text-align:right">Showing 30 of ${rows.length} entries</div>` : ''}` : ''}
|
|
3120
3326
|
`;
|
|
3121
3327
|
} catch (e) {
|
|
3122
3328
|
content.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>';
|
|
@@ -3138,14 +3344,15 @@ async function loadMemADRs() {
|
|
|
3138
3344
|
function renderADRs(list) {
|
|
3139
3345
|
const pane = document.getElementById('adr-content');
|
|
3140
3346
|
if (!pane) return;
|
|
3141
|
-
if (!list.length) { pane.innerHTML = '<div class="empty">No ADRs found</div>'; return; }
|
|
3347
|
+
if (!list.length) { pane.innerHTML = '<div class="empty">No ADRs found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">ADRs are created by agents via <code>monomind memory store --type adr</code></div></div>'; return; }
|
|
3142
3348
|
pane.innerHTML = list.slice(0, 50).map(a => `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
|
|
3143
3349
|
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px">
|
|
3144
3350
|
<span style="color:var(--text-hi);font-size:12px;font-family:monospace">${esc(a.id || a.title || '—')}</span>
|
|
3145
3351
|
<span class="ss-pill ${a.status === 'accepted' ? 'on' : a.status === 'deprecated' ? 'warn' : ''}">${esc(a.status || '?')}</span>
|
|
3146
3352
|
</div>
|
|
3147
|
-
<div style="font-size:11px;color:var(--text-lo)">${esc((a.context || a.summary || '').slice(0, 120))}</div>
|
|
3148
|
-
</div>`).join('')
|
|
3353
|
+
<div style="font-size:11px;color:var(--text-lo)" title="${esc(a.context || a.summary || '')}">${esc((a.context || a.summary || '').slice(0, 120))}${(a.context || a.summary || '').length > 120 ? '…' : ''}</div>
|
|
3354
|
+
</div>`).join('') +
|
|
3355
|
+
(list.length > 50 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:8px;text-align:right">Showing 50 of ${list.length} ADRs</div>` : '');
|
|
3149
3356
|
}
|
|
3150
3357
|
|
|
3151
3358
|
function filterADRs(q) {
|
|
@@ -3160,11 +3367,15 @@ function filterADRs(q) {
|
|
|
3160
3367
|
|
|
3161
3368
|
function renderMiniSessions(sessions) {
|
|
3162
3369
|
if (!sessions.length) return;
|
|
3163
|
-
const items = sessions.map((s, i) =>
|
|
3370
|
+
const items = sessions.map((s, i) => {
|
|
3371
|
+
const costStr = typeof s.totalCost === 'number' && s.totalCost > 0.001 ? ' · $' + s.totalCost.toFixed(2) : '';
|
|
3372
|
+
const durStr = s.totalDurationMs ? ' · ' + fmtDur(s.totalDurationMs) : '';
|
|
3373
|
+
return `
|
|
3164
3374
|
<div class="mini-sess" onclick="sessionIdx=${i};userScrolled=false;loadFeedForSession(allSessions[${i}])">
|
|
3165
|
-
<div class="ms-prompt">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
|
|
3166
|
-
<div class="ms-meta">${relTime(s.lastTs || s.mtime)}</div>
|
|
3167
|
-
</div
|
|
3375
|
+
<div class="ms-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
|
|
3376
|
+
<div class="ms-meta"><span title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(s.lastTs || s.mtime)}">${relTime(s.lastTs || s.mtime)}</span>${durStr}${costStr}</div>
|
|
3377
|
+
</div>`;
|
|
3378
|
+
}).join('');
|
|
3168
3379
|
document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
|
|
3169
3380
|
buildSwimlane();
|
|
3170
3381
|
}
|
|
@@ -3191,7 +3402,7 @@ function renderProjectGrid(projects, query) {
|
|
|
3191
3402
|
(p.name || p.slug || '').toLowerCase().includes(query.toLowerCase()) ||
|
|
3192
3403
|
(p.path || '').toLowerCase().includes(query.toLowerCase())) : projects;
|
|
3193
3404
|
if (!filtered.length) {
|
|
3194
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div></div>';
|
|
3405
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Try a different search term or clear the filter</div></div>';
|
|
3195
3406
|
return;
|
|
3196
3407
|
}
|
|
3197
3408
|
el.className = 'proj-grid';
|
|
@@ -3202,12 +3413,12 @@ function renderProjectGrid(projects, query) {
|
|
|
3202
3413
|
return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
|
|
3203
3414
|
${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
|
|
3204
3415
|
<div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
|
|
3205
|
-
<div class="proj-card-name">${esc(p.name || p.slug)}</div>
|
|
3206
|
-
<div class="proj-card-path">${esc(p.path || '')}</div>
|
|
3416
|
+
<div class="proj-card-name" title="${esc(p.name || p.slug || '')}">${esc(p.name || p.slug)}</div>
|
|
3417
|
+
<div class="proj-card-path" title="${esc(p.path || '')}">${esc(shortPath(p.path || ''))}</div>
|
|
3207
3418
|
<div class="proj-card-stats">
|
|
3208
3419
|
<div class="proj-stat"><div class="ps-v">${p.sessionCount || 0}</div><div class="ps-l">sessions</div></div>
|
|
3209
3420
|
<div class="proj-stat"><div class="ps-v">${p.memoryCount || 0}</div><div class="ps-l">memories</div></div>
|
|
3210
|
-
${p.lastActivity ? `<div class="proj-stat"><div class="ps-v" style="font-size:12px">${relTime(p.lastActivity)}</div><div class="ps-l">last active</div></div>` : ''}
|
|
3421
|
+
${p.lastActivity ? `<div class="proj-stat"><div class="ps-v" style="font-size:12px" title="${new Date(typeof p.lastActivity === 'number' ? p.lastActivity : Number(p.lastActivity) || p.lastActivity).toLocaleString()}">${relTime(p.lastActivity)}</div><div class="ps-l">last active</div></div>` : ''}
|
|
3211
3422
|
</div>
|
|
3212
3423
|
</div>`;
|
|
3213
3424
|
}).join('');
|
|
@@ -3229,19 +3440,33 @@ async function renderSessions() {
|
|
|
3229
3440
|
document.getElementById('sess-pg-sub').textContent =
|
|
3230
3441
|
sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
|
|
3231
3442
|
if (!sessions.length) {
|
|
3232
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
|
|
3443
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Start a session with <code>npx monomind agent spawn</code></div></div>';
|
|
3233
3444
|
return;
|
|
3234
3445
|
}
|
|
3235
3446
|
let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
|
|
3236
3447
|
if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
|
|
3237
3448
|
if (heatmapDateFilter) toShow = toShow.filter(s => {
|
|
3238
3449
|
const t = s.lastTs || s.mtime; if (!t) return false;
|
|
3239
|
-
return new Date(typeof t === 'number' ? t : t).toDateString() === heatmapDateFilter;
|
|
3450
|
+
return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString() === heatmapDateFilter;
|
|
3240
3451
|
});
|
|
3241
3452
|
// f57: file pivot filter
|
|
3242
3453
|
if (filePivot) toShow = toShow.filter(s => (s.filesTouched || []).includes(filePivot));
|
|
3243
3454
|
if (!toShow.length) {
|
|
3244
|
-
|
|
3455
|
+
let emptyMsg, emptyHint;
|
|
3456
|
+
if (filePivot) {
|
|
3457
|
+
emptyMsg = 'No sessions touching ' + esc(filePivot.split('/').pop());
|
|
3458
|
+
emptyHint = 'Clear the file filter to see all sessions';
|
|
3459
|
+
} else if (heatmapDateFilter) {
|
|
3460
|
+
emptyMsg = 'No sessions on ' + esc(heatmapDateFilter);
|
|
3461
|
+
emptyHint = 'Click another date on the heatmap or clear the filter';
|
|
3462
|
+
} else if (activeTagFilter) {
|
|
3463
|
+
emptyMsg = 'No sessions tagged "' + esc(activeTagFilter) + '"';
|
|
3464
|
+
emptyHint = 'Clear the tag filter to see all sessions';
|
|
3465
|
+
} else {
|
|
3466
|
+
emptyMsg = 'No bookmarked sessions';
|
|
3467
|
+
emptyHint = 'Click the ☆ on any session row to bookmark it.';
|
|
3468
|
+
}
|
|
3469
|
+
el.innerHTML = `<div class="empty"><div class="empty-ico">☆</div><div>${emptyMsg}</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">${emptyHint}</div></div>`;
|
|
3245
3470
|
buildSessionHeatmap(sessions);
|
|
3246
3471
|
return;
|
|
3247
3472
|
}
|
|
@@ -3253,7 +3478,7 @@ async function renderSessions() {
|
|
|
3253
3478
|
const now = Date.now(); const DAY = 86400000;
|
|
3254
3479
|
function sessDateGroup(s) {
|
|
3255
3480
|
const t = s.lastTs || s.mtime; if (!t) return 'Older';
|
|
3256
|
-
const age = now - (typeof t === 'number' ? t : new Date(t).getTime());
|
|
3481
|
+
const age = now - (typeof t === 'number' ? t : Number(t) || new Date(t).getTime() || 0);
|
|
3257
3482
|
if (age < DAY) return 'Today';
|
|
3258
3483
|
if (age < 2 * DAY) return 'Yesterday';
|
|
3259
3484
|
if (age < 8 * DAY) return 'This week';
|
|
@@ -3288,7 +3513,7 @@ async function renderSessions() {
|
|
|
3288
3513
|
const summaries = (s.summaries || []).slice(0, 2).map(sm => { const t = typeof sm === 'string' ? sm : (sm.summary || sm.text || String(sm)); return `<span class="sr-tag">${esc(t.slice(0, 40))}</span>`; }).join('');
|
|
3289
3514
|
const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
|
|
3290
3515
|
const isStarred = bookmarks.has(s.id);
|
|
3291
|
-
const sData = JSON.stringify(s).replace(/'/g, ''');
|
|
3516
|
+
const sData = JSON.stringify(s).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, ''');
|
|
3292
3517
|
const note = getSessNote(s.id);
|
|
3293
3518
|
const hasNote = !!note;
|
|
3294
3519
|
const files = (s.filesTouched || []).slice(0, 5);
|
|
@@ -3313,12 +3538,14 @@ async function renderSessions() {
|
|
|
3313
3538
|
? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
|
|
3314
3539
|
: '';
|
|
3315
3540
|
const summaryHtml = s.summary
|
|
3316
|
-
? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
|
|
3541
|
+
? `<div class="sr-summary" title="${esc(s.summary)}">${esc(s.summary.slice(0, 180))}${s.summary.length > 180 ? '…' : ''}</div>`
|
|
3317
3542
|
: '';
|
|
3543
|
+
const srTimeTs = s.lastTs || s.mtime;
|
|
3544
|
+
const srTimeFull = srTimeTs ? new Date(typeof srTimeTs === 'number' ? srTimeTs : Number(srTimeTs) || srTimeTs).toLocaleString() : '';
|
|
3318
3545
|
return `<div class="sess-row" data-sess-idx="${idx}" data-sess-id="${esc(s.id)}" onclick="handleSessRowClick(event,this,'${esc(s.id)}')" data-sess-data='${sData}'>
|
|
3319
3546
|
<div class="sr-top">
|
|
3320
|
-
<div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
|
|
3321
|
-
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
|
|
3547
|
+
<div class="sr-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
|
|
3548
|
+
<div class="sr-time"${srTimeFull ? ` title="${esc(srTimeFull)}"` : ''}>${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
|
|
3322
3549
|
<button class="sr-copy-btn" data-prompt="${esc(s.lastPrompt || s.id)}" onclick="copyPrompt(this.dataset.prompt,event)" title="Copy prompt to clipboard">⎘</button>
|
|
3323
3550
|
<button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(s.id)}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
|
|
3324
3551
|
<span class="sr-view">→ view</span>
|
|
@@ -3332,7 +3559,7 @@ async function renderSessions() {
|
|
|
3332
3559
|
${ctxGauge}
|
|
3333
3560
|
<div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
|
|
3334
3561
|
<div class="sess-notes-wrap" onclick="event.stopPropagation()">
|
|
3335
|
-
<button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
|
|
3562
|
+
<button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" title="${hasNote ? 'Show/hide session note' : 'Add a note to this session'}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
|
|
3336
3563
|
<div class="sess-notes-area" id="snote-${esc(s.id)}">
|
|
3337
3564
|
<textarea class="sess-note-input" rows="2" placeholder="Session note…" oninput="saveSessNote('${esc(s.id)}',this.value,this.closest('.sess-notes-wrap').querySelector('.sess-notes-toggle'),this.closest('.sess-row').querySelector('.sess-note-saved'))">${esc(note)}</textarea>
|
|
3338
3565
|
<div class="sess-note-saved"></div>
|
|
@@ -3373,14 +3600,14 @@ async function renderSessions() {
|
|
|
3373
3600
|
const todayCost = allSessions.filter(s => {
|
|
3374
3601
|
const t = s.firstTs || s.mtime;
|
|
3375
3602
|
if (!t) return false;
|
|
3376
|
-
const d = new Date(typeof t === 'number' ? t : t);
|
|
3603
|
+
const d = new Date(typeof t === 'number' ? t : Number(t) || t);
|
|
3377
3604
|
const now = new Date();
|
|
3378
3605
|
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
|
|
3379
3606
|
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
3380
3607
|
const monthCost = allSessions.filter(s => {
|
|
3381
3608
|
const t = s.firstTs || s.mtime;
|
|
3382
3609
|
if (!t) return false;
|
|
3383
|
-
const d = new Date(typeof t === 'number' ? t : t);
|
|
3610
|
+
const d = new Date(typeof t === 'number' ? t : Number(t) || t);
|
|
3384
3611
|
const now = new Date();
|
|
3385
3612
|
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
|
|
3386
3613
|
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
@@ -3459,7 +3686,7 @@ function buildTagFilterBar(sessions) {
|
|
|
3459
3686
|
if (!allTags.common.size) return '';
|
|
3460
3687
|
const sorted = [...allTags.common].sort();
|
|
3461
3688
|
const chips = sorted.map(t =>
|
|
3462
|
-
`<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
|
|
3689
|
+
`<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" title="Filter sessions by tag: ${esc(t)}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
|
|
3463
3690
|
).join('');
|
|
3464
3691
|
return `<div class="tag-filter-bar">${chips}</div>`;
|
|
3465
3692
|
}
|
|
@@ -3488,10 +3715,12 @@ function buildRecap(events, sess) {
|
|
|
3488
3715
|
const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
|
|
3489
3716
|
|
|
3490
3717
|
const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
|
|
3718
|
+
const durStr = sess?.totalDurationMs ? fmtDur(sess.totalDurationMs) : null;
|
|
3491
3719
|
|
|
3492
3720
|
const stats = [
|
|
3493
3721
|
tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
|
|
3494
3722
|
users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
|
|
3723
|
+
durStr ? `<span class="recap-stat">${durStr}</span>` : '',
|
|
3495
3724
|
costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
|
|
3496
3725
|
errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
|
|
3497
3726
|
].filter(Boolean).join('');
|
|
@@ -3507,12 +3736,15 @@ async function renderGlobalFeed() {
|
|
|
3507
3736
|
try {
|
|
3508
3737
|
// fetch project list
|
|
3509
3738
|
const data = await apiFetch('/api/projects');
|
|
3510
|
-
const
|
|
3739
|
+
const allProjects = data?.projects || [];
|
|
3740
|
+
const projects = allProjects.slice(0, 8);
|
|
3511
3741
|
if (!projects.length) {
|
|
3512
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
|
|
3742
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Run <code>npx monomind init</code> inside a project to register it.</div></div>';
|
|
3513
3743
|
return;
|
|
3514
3744
|
}
|
|
3515
|
-
document.getElementById('gf-sub').textContent =
|
|
3745
|
+
document.getElementById('gf-sub').textContent = allProjects.length > 8
|
|
3746
|
+
? `Last activity across ${projects.length} of ${allProjects.length} projects`
|
|
3747
|
+
: `Last activity across ${projects.length} project${projects.length !== 1 ? 's' : ''}`;
|
|
3516
3748
|
|
|
3517
3749
|
// fetch sessions for each project in parallel
|
|
3518
3750
|
const results = await Promise.allSettled(
|
|
@@ -3531,11 +3763,11 @@ async function renderGlobalFeed() {
|
|
|
3531
3763
|
entries.sort((a, b) => {
|
|
3532
3764
|
const ta = a.session.lastTs || a.session.mtime || 0;
|
|
3533
3765
|
const tb = b.session.lastTs || b.session.mtime || 0;
|
|
3534
|
-
return (typeof tb === 'number' ? tb : new Date(tb).getTime()) - (typeof ta === 'number' ? ta : new Date(ta).getTime());
|
|
3766
|
+
return (typeof tb === 'number' ? tb : Number(tb) || new Date(tb).getTime() || 0) - (typeof ta === 'number' ? ta : Number(ta) || new Date(ta).getTime() || 0);
|
|
3535
3767
|
});
|
|
3536
3768
|
|
|
3537
3769
|
if (!entries.length) {
|
|
3538
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div></div>';
|
|
3770
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Sessions appear when Claude Code runs inside registered projects</div></div>';
|
|
3539
3771
|
return;
|
|
3540
3772
|
}
|
|
3541
3773
|
|
|
@@ -3548,12 +3780,12 @@ async function renderGlobalFeed() {
|
|
|
3548
3780
|
? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
|
|
3549
3781
|
: '';
|
|
3550
3782
|
const gfSummaryHtml = s.summary
|
|
3551
|
-
? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
|
|
3783
|
+
? `<div class="sr-summary" title="${esc(s.summary)}">${esc(s.summary.slice(0, 180))}${s.summary.length > 180 ? '…' : ''}</div>`
|
|
3552
3784
|
: '';
|
|
3553
3785
|
return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
|
|
3554
3786
|
<div class="sr-top">
|
|
3555
|
-
<div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
|
|
3556
|
-
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${gfCompactBadge}
|
|
3787
|
+
<div class="sr-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
|
|
3788
|
+
<div class="sr-time" title="${(t => t ? new Date(typeof t === 'number' ? t : Number(t) || t).toLocaleString() : '')(s.lastTs || s.mtime)}">${relTime(s.lastTs || s.mtime)}</div>${gfCompactBadge}
|
|
3557
3789
|
<span class="gf-proj-tag">${esc(projName)}</span>
|
|
3558
3790
|
</div>
|
|
3559
3791
|
${gfSummaryHtml}
|
|
@@ -3565,8 +3797,232 @@ async function renderGlobalFeed() {
|
|
|
3565
3797
|
}
|
|
3566
3798
|
}
|
|
3567
3799
|
|
|
3800
|
+
// ── global loops (multi-project) ───────────────────────────
|
|
3801
|
+
function deduplicateLoops(loops) {
|
|
3802
|
+
const hasRepeat = loops.some(l => l.source === '_repeat.md');
|
|
3803
|
+
const repeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
|
|
3804
|
+
return loops.filter(l => {
|
|
3805
|
+
if (l.source === 'scheduled_tasks_lock' && hasRepeat) return false;
|
|
3806
|
+
if (l.source !== 'schedule_wakeup_hook') return true;
|
|
3807
|
+
const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
|
|
3808
|
+
return !m || !repeatPrompts.has(m[1].trim());
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
async function renderGlobalLoops() {
|
|
3813
|
+
const el = document.getElementById('gl-content');
|
|
3814
|
+
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
3815
|
+
try {
|
|
3816
|
+
const data = await apiFetch('/api/projects');
|
|
3817
|
+
const allProjects = data?.projects || [];
|
|
3818
|
+
const projects = allProjects.slice(0, 20);
|
|
3819
|
+
if (!projects.length) {
|
|
3820
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No projects found</div></div>';
|
|
3821
|
+
document.getElementById('bdg-global-loops').textContent = '—';
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
const results = await Promise.allSettled(
|
|
3826
|
+
projects.map(p => apiFetch('/api/loops?dir=' + enc(p.path)).then(d => ({
|
|
3827
|
+
project: p,
|
|
3828
|
+
loops: deduplicateLoops(Array.isArray(d) ? d : (d.loops || []))
|
|
3829
|
+
})))
|
|
3830
|
+
);
|
|
3831
|
+
|
|
3832
|
+
let totalLoops = 0;
|
|
3833
|
+
const sections = [];
|
|
3834
|
+
for (const r of results) {
|
|
3835
|
+
if (r.status !== 'fulfilled') continue;
|
|
3836
|
+
const { project, loops } = r.value;
|
|
3837
|
+
if (!loops.length) continue;
|
|
3838
|
+
totalLoops += loops.length;
|
|
3839
|
+
const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
|
|
3840
|
+
const rows = loops.map((l, idx) => {
|
|
3841
|
+
const isTillend = l.type === 'tillend';
|
|
3842
|
+
const curRep = l.currentRep || 0;
|
|
3843
|
+
const maxReps = l.maxReps || 0;
|
|
3844
|
+
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
|
|
3845
|
+
const isHil = l.status === 'hil:pending';
|
|
3846
|
+
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
3847
|
+
const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
|
|
3848
|
+
const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive &&
|
|
3849
|
+
nextAt > 0 && nextAt <= Date.now();
|
|
3850
|
+
const isStaledActive = isExplicitlyActive && nextAt > 0 &&
|
|
3851
|
+
(Date.now() - nextAt) > LOOP_STALE_MS;
|
|
3852
|
+
const isFinished = isOverdue || isStaledActive ||
|
|
3853
|
+
(!isExplicitlyActive && maxReps > 0 && curRep >= maxReps) ||
|
|
3854
|
+
['finished','done','complete','completed','expired'].includes(l.status);
|
|
3855
|
+
const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
|
|
3856
|
+
const intervalStr = fmtInterval(l.interval || l.schedule);
|
|
3857
|
+
const _lp = (function(_l) {
|
|
3858
|
+
if (_l.command) {
|
|
3859
|
+
const flags = [];
|
|
3860
|
+
if (_l.type && _l.type !== 'repeat') flags.push('--' + _l.type);
|
|
3861
|
+
if (_l.maxReps) flags.push('--maxruns ' + _l.maxReps);
|
|
3862
|
+
if (_l.wait || _l.interval) flags.push('--wait ' + (_l.wait || _l.interval * 60));
|
|
3863
|
+
if (_l.currentRep != null) flags.push('--rep ' + _l.currentRep);
|
|
3864
|
+
if (_l.id) flags.push('--loop ' + _l.id);
|
|
3865
|
+
return { userPrompt: _l.prompt || '', command: _l.command, flagsStr: flags.join(' ') };
|
|
3866
|
+
}
|
|
3867
|
+
const full = _l.prompt || '';
|
|
3868
|
+
const cmdM = full.match(/^(\/\S+)/);
|
|
3869
|
+
if (!cmdM) return { userPrompt: full, command: '', flagsStr: '' };
|
|
3870
|
+
const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
|
|
3871
|
+
let ti = 0, fp = [];
|
|
3872
|
+
while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
|
|
3873
|
+
fp.push(tokens[ti++]);
|
|
3874
|
+
if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) fp.push(tokens[ti++]);
|
|
3875
|
+
}
|
|
3876
|
+
return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1], flagsStr: fp.join(' ') };
|
|
3877
|
+
})(l);
|
|
3878
|
+
const userPrompt = _lp.userPrompt;
|
|
3879
|
+
const cmdStr = _lp.command;
|
|
3880
|
+
const flagsStr = _lp.flagsStr;
|
|
3881
|
+
const fullPrompt = l.prompt || '';
|
|
3882
|
+
const name = (l.name || userPrompt || cmdStr || 'loop').slice(0, 60);
|
|
3883
|
+
const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
|
|
3884
|
+
const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
|
|
3885
|
+
const pct = (!isTillend && maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
|
|
3886
|
+
const progBar = (!isTillend && maxReps > 0 && running)
|
|
3887
|
+
? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>` : '';
|
|
3888
|
+
const runCountDisplay = isTillend
|
|
3889
|
+
? `run ${curRep} / ∞${maxReps > 0 ? ' (cap: ' + maxReps + ')' : ''}`
|
|
3890
|
+
: (maxReps > 0 ? `${curRep} / ${maxReps}` : String(curRep || '—'));
|
|
3891
|
+
const cdownSpan = nextAt
|
|
3892
|
+
? ` <span class="loop-cdown${nextAt - Date.now() <= 0 ? ' overdue' : ''}" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>` : '';
|
|
3893
|
+
const stopBtn = running
|
|
3894
|
+
? `<button class="loop-stop-btn" data-loop-id="${esc(l.id || l.name || String(idx))}" onclick="stopLoop(event, this.dataset.loopId)" title="Stop this loop">■ Stop</button>` : '';
|
|
3895
|
+
const typeBadge = `<span class="loop-type-badge${isTillend ? ' tillend' : ''}">${esc(l.type || 'repeat')}</span>`;
|
|
3896
|
+
const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
|
|
3897
|
+
const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
|
|
3898
|
+
const hilBanner = isHil
|
|
3899
|
+
? `<div class="loop-hil-banner">⚠ Waiting for human response — open HIL file to resume</div>` : '';
|
|
3900
|
+
const metaParts = [intervalStr, l.description].filter(Boolean).join(' · ').slice(0, 80);
|
|
3901
|
+
return `<div class="loop-row" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
|
|
3902
|
+
<div class="loop-ico">${isTillend ? '∞' : '↺'}</div>
|
|
3903
|
+
<div class="loop-body">
|
|
3904
|
+
<div class="loop-name" title="${esc(userPrompt || fullPrompt)}">${typeBadge}${esc(name)}</div>
|
|
3905
|
+
<div class="loop-meta">${esc(metaParts)}${cdownSpan}</div>
|
|
3906
|
+
${hilBanner}
|
|
3907
|
+
${progBar}
|
|
3908
|
+
</div>
|
|
3909
|
+
<div class="loop-status ${statusClass}">${statusLabel}</div>
|
|
3910
|
+
${stopBtn}
|
|
3911
|
+
</div>
|
|
3912
|
+
<div class="loop-expand">
|
|
3913
|
+
${userPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(userPrompt)}">${esc(userPrompt.slice(0, 300))}${userPrompt.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy prompt" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(userPrompt)}).then(()=>showToast('Copied','Prompt copied','ok'))">⎘</button></div></div>` : ''}
|
|
3914
|
+
${cmdStr ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="display:flex;align-items:center;gap:8px"><span>${esc(cmdStr)}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy command" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(cmdStr)}).then(()=>showToast('Copied','Command copied','ok'))">⎘</button></div></div>` : ''}
|
|
3915
|
+
${flagsStr ? `<div class="le-row"><div class="le-lbl">Flags</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(flagsStr)}">${esc(flagsStr.slice(0, 300))}${flagsStr.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy flags" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(flagsStr)}).then(()=>showToast('Copied','Flags copied','ok'))">⎘</button></div></div>` : ''}
|
|
3916
|
+
<div class="le-row"><div class="le-lbl">Project</div><div class="le-val"><span class="gf-proj-tag">${esc(projName)}</span></div></div>
|
|
3917
|
+
<div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${esc(l.type || 'repeat')}</div></div>
|
|
3918
|
+
<div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(intervalStr || '—')}</div></div>
|
|
3919
|
+
<div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isHil ? '⚠ hil:pending' : (running ? '● running' : (isFinished ? '✓ done' : '○ stopped'))}</div></div>
|
|
3920
|
+
${isHil && l.id ? `<div class="le-row"><div class="le-lbl">HIL file</div><div class="le-val mono" style="color:oklch(75% 0.16 60);word-break:break-all">.monomind/loops/${esc(l.id)}-hil.md</div></div>` : ''}
|
|
3921
|
+
<div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
|
|
3922
|
+
${(()=>{ const sMs=l.startedAt?(typeof l.startedAt==='number'?l.startedAt:new Date(l.startedAt).getTime()):0; const age=sMs>0&&sMs<Date.now()?Date.now()-sMs:0; return age>0?`<div class="le-row"><div class="le-lbl">Running for</div><div class="le-val">${fmtDur(age)}</div></div>`:''; })()}
|
|
3923
|
+
<div class="le-row"><div class="le-lbl">Last run</div><div class="le-val" title="${l.lastRunAt ? new Date(typeof l.lastRunAt === 'number' ? l.lastRunAt : Number(l.lastRunAt) || l.lastRunAt).toLocaleString() : ''}">${esc(lastRun)}</div></div>
|
|
3924
|
+
<div class="le-row"><div class="le-lbl">${isTillend ? 'Progress' : 'Run count'}</div><div class="le-val">${esc(runCountDisplay)}</div></div>
|
|
3925
|
+
${l.source ? `<div class="le-row"><div class="le-lbl">Source</div><div class="le-val">${esc(l.source)}</div></div>` : ''}
|
|
3926
|
+
${buildLoopSparkline(l)}
|
|
3927
|
+
</div>`;
|
|
3928
|
+
}).join('');
|
|
3929
|
+
sections.push(`<div style="margin-bottom:18px">
|
|
3930
|
+
<div class="m-group-title" style="margin-bottom:6px">${esc(projName)}</div>
|
|
3931
|
+
${rows}
|
|
3932
|
+
</div>`);
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
document.getElementById('bdg-global-loops').textContent = totalLoops || '—';
|
|
3936
|
+
document.getElementById('gl-sub').textContent = `${totalLoops} loop${totalLoops !== 1 ? 's' : ''} across ${sections.length} project${sections.length !== 1 ? 's' : ''}`;
|
|
3937
|
+
|
|
3938
|
+
if (!sections.length) {
|
|
3939
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops found across projects</div></div>';
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
el.innerHTML = sections.join('');
|
|
3943
|
+
startCountdowns();
|
|
3944
|
+
} catch (err) {
|
|
3945
|
+
el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
// ── global tokens (multi-project) ──────────────────────────
|
|
3950
|
+
async function renderGlobalTokens() {
|
|
3951
|
+
const el = document.getElementById('gt-content');
|
|
3952
|
+
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
3953
|
+
try {
|
|
3954
|
+
const data = await apiFetch('/api/projects');
|
|
3955
|
+
const allProjects = data?.projects || [];
|
|
3956
|
+
const projects = allProjects.slice(0, 20);
|
|
3957
|
+
if (!projects.length) {
|
|
3958
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">$</div><div>No projects found</div></div>';
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
const results = await Promise.allSettled(
|
|
3963
|
+
projects.map(p => apiFetch('/api/section?name=tokens&dir=' + enc(p.path)).then(d => ({
|
|
3964
|
+
project: p,
|
|
3965
|
+
tokens: d?.tokens?.summary || {}
|
|
3966
|
+
})))
|
|
3967
|
+
);
|
|
3968
|
+
|
|
3969
|
+
let aggTodayCost = 0, aggMonthCost = 0, aggTotalTokens = 0;
|
|
3970
|
+
const rows = [];
|
|
3971
|
+
for (const r of results) {
|
|
3972
|
+
if (r.status !== 'fulfilled') continue;
|
|
3973
|
+
const { project, tokens: s } = r.value;
|
|
3974
|
+
const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
|
|
3975
|
+
const todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
|
|
3976
|
+
const monthCost = typeof s.monthCost === 'number' ? s.monthCost : 0;
|
|
3977
|
+
const totalTokens = s.totalTokens != null ? Number(s.totalTokens) : 0;
|
|
3978
|
+
const totalCost = typeof s.totalCost === 'number' ? s.totalCost : (monthCost || todayCost);
|
|
3979
|
+
aggTodayCost += todayCost;
|
|
3980
|
+
aggMonthCost += monthCost;
|
|
3981
|
+
aggTotalTokens += totalTokens;
|
|
3982
|
+
rows.push({ projName, todayCost, monthCost, totalTokens, totalCost });
|
|
3983
|
+
}
|
|
3984
|
+
rows.sort((a, b) => b.totalCost - a.totalCost);
|
|
3985
|
+
|
|
3986
|
+
const cards = [
|
|
3987
|
+
{ label: 'Today Cost', val: '$' + aggTodayCost.toFixed(2) },
|
|
3988
|
+
{ label: 'Month Cost', val: '$' + aggMonthCost.toFixed(2) },
|
|
3989
|
+
{ label: 'Total Tokens', val: aggTotalTokens > 0 ? Number(aggTotalTokens).toLocaleString() : '—' },
|
|
3990
|
+
].map(c => `<div class="tok-card"><div class="tc-label">${esc(c.label)}</div><div class="tc-val">${esc(c.val)}</div></div>`).join('');
|
|
3991
|
+
|
|
3992
|
+
const tableRows = rows.map(r => `<tr style="border-top:1px solid var(--border)">
|
|
3993
|
+
<td style="padding:4px 8px 4px 0;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.projName)}">${esc(r.projName)}</td>
|
|
3994
|
+
<td style="padding:4px 8px;color:var(--accent)">$${r.todayCost.toFixed(2)}</td>
|
|
3995
|
+
<td style="padding:4px 8px;color:var(--text-lo)">$${r.monthCost.toFixed(2)}</td>
|
|
3996
|
+
<td style="padding:4px 8px;color:var(--text-lo)">${r.totalTokens > 0 ? Number(r.totalTokens).toLocaleString() : '—'}</td>
|
|
3997
|
+
<td style="padding:4px 8px;color:var(--accent)">$${r.totalCost.toFixed(2)}</td>
|
|
3998
|
+
</tr>`).join('');
|
|
3999
|
+
|
|
4000
|
+
const projectCount = rows.length;
|
|
4001
|
+
document.getElementById('gt-sub').textContent = allProjects.length > 20
|
|
4002
|
+
? `Token usage across ${projectCount} of ${allProjects.length} projects`
|
|
4003
|
+
: `Token usage across ${projectCount} project${projectCount !== 1 ? 's' : ''}`;
|
|
4004
|
+
|
|
4005
|
+
el.innerHTML = `<div id="gt-cards" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px">${cards}</div>
|
|
4006
|
+
<div id="gt-table">
|
|
4007
|
+
<div class="m-group-title" style="margin-bottom:6px">Projects by cost</div>
|
|
4008
|
+
<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px;font-family:monospace">
|
|
4009
|
+
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
4010
|
+
<th style="padding:4px 8px 4px 0">Project</th>
|
|
4011
|
+
<th style="padding:4px 8px">Today</th>
|
|
4012
|
+
<th style="padding:4px 8px">Month</th>
|
|
4013
|
+
<th style="padding:4px 8px">Total Tokens</th>
|
|
4014
|
+
<th style="padding:4px 8px">Total Cost</th>
|
|
4015
|
+
</tr></thead>
|
|
4016
|
+
<tbody>${tableRows}</tbody>
|
|
4017
|
+
</table></div>
|
|
4018
|
+
</div>`;
|
|
4019
|
+
} catch (err) {
|
|
4020
|
+
el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
|
|
3568
4024
|
// ── feature 4: budget cap + desktop notification ───────────
|
|
3569
|
-
let budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
|
|
4025
|
+
let budget = (function(){ try { return JSON.parse(localStorage.getItem('mm-budget') || '{}'); } catch { return {}; } })();
|
|
3570
4026
|
|
|
3571
4027
|
function openBudgetModal() {
|
|
3572
4028
|
const b = budget;
|
|
@@ -3589,6 +4045,7 @@ function saveBudget() {
|
|
|
3589
4045
|
closeBudgetModal();
|
|
3590
4046
|
checkBudget(); // check immediately
|
|
3591
4047
|
updateBudgetBtnStyle();
|
|
4048
|
+
showToast('Budget saved', budget.daily || budget.monthly ? `Daily: ${budget.daily ? '$'+budget.daily : '—'} · Monthly: ${budget.monthly ? '$'+budget.monthly : '—'}` : 'Budget cleared', 'ok');
|
|
3592
4049
|
}
|
|
3593
4050
|
|
|
3594
4051
|
function updateBudgetBtnStyle() {
|
|
@@ -3600,21 +4057,36 @@ function updateBudgetBtnStyle() {
|
|
|
3600
4057
|
|
|
3601
4058
|
function checkBudget() {
|
|
3602
4059
|
const cost = alertState.todayCost;
|
|
3603
|
-
|
|
3604
|
-
|
|
4060
|
+
const moCost = alertState.monthCost;
|
|
4061
|
+
// Daily budget check
|
|
4062
|
+
if (budget.daily && cost) {
|
|
3605
4063
|
const pct = cost / budget.daily;
|
|
3606
4064
|
if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
|
|
3607
4065
|
alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
3608
4066
|
alertState.budgetCls = 'alert-crit';
|
|
4067
|
+
updateAlerts(); return;
|
|
3609
4068
|
} else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
|
|
3610
4069
|
alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
3611
4070
|
alertState.budgetCls = 'alert-warn';
|
|
3612
4071
|
maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
|
|
3613
|
-
|
|
3614
|
-
|
|
4072
|
+
updateAlerts(); return;
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
// Monthly budget check
|
|
4076
|
+
if (budget.monthly && moCost) {
|
|
4077
|
+
const mpct = moCost / budget.monthly;
|
|
4078
|
+
if (mpct >= 1 && !dismissedAlerts.has('budget-monthly-over')) {
|
|
4079
|
+
alertState.budgetAlert = `Monthly budget exceeded: $${moCost.toFixed(2)} / $${budget.monthly}`;
|
|
4080
|
+
alertState.budgetCls = 'alert-crit';
|
|
4081
|
+
updateAlerts(); return;
|
|
4082
|
+
} else if (mpct >= 0.8 && !dismissedAlerts.has('budget-monthly-warn')) {
|
|
4083
|
+
alertState.budgetAlert = `Approaching monthly budget: $${moCost.toFixed(2)} / $${budget.monthly}`;
|
|
4084
|
+
alertState.budgetCls = 'alert-warn';
|
|
4085
|
+
updateAlerts(); return;
|
|
3615
4086
|
}
|
|
3616
|
-
updateAlerts();
|
|
3617
4087
|
}
|
|
4088
|
+
alertState.budgetAlert = null;
|
|
4089
|
+
updateAlerts();
|
|
3618
4090
|
}
|
|
3619
4091
|
|
|
3620
4092
|
function maybeNotify(title, body) {
|
|
@@ -3695,7 +4167,7 @@ function computeHealthScore(p) {
|
|
|
3695
4167
|
const DAY = 86400000;
|
|
3696
4168
|
// recency: up to +30 points for activity in last 7 days
|
|
3697
4169
|
if (p.lastActivity) {
|
|
3698
|
-
const age = now - (typeof p.lastActivity === 'number' ? p.lastActivity : new Date(p.lastActivity).getTime());
|
|
4170
|
+
const age = now - (typeof p.lastActivity === 'number' ? p.lastActivity : Number(p.lastActivity) || new Date(p.lastActivity).getTime() || 0);
|
|
3699
4171
|
if (age < DAY) score += 30;
|
|
3700
4172
|
else if (age < 3*DAY) score += 20;
|
|
3701
4173
|
else if (age < 7*DAY) score += 10;
|
|
@@ -3736,7 +4208,7 @@ function buildBreakdownByName(events) {
|
|
|
3736
4208
|
const pct = Math.round(cnt / total * 100);
|
|
3737
4209
|
return `<div class="tb-row">
|
|
3738
4210
|
<div class="tb-lbl" style="width:54px" title="${esc(name)}">${esc(name.length > 8 ? name.slice(0,7)+'…' : name)}</div>
|
|
3739
|
-
<div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
|
|
4211
|
+
<div class="tb-bar-wrap" title="${esc(name)}: ${pct}% (${cnt} call${cnt!==1?'s':''})"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
|
|
3740
4212
|
<div class="tb-count">${cnt}</div>
|
|
3741
4213
|
</div>`;
|
|
3742
4214
|
}).join('');
|
|
@@ -3760,7 +4232,7 @@ function buildDigest() {
|
|
|
3760
4232
|
const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
|
|
3761
4233
|
const todaySessions = allSessions.filter(s => {
|
|
3762
4234
|
const t = s.lastTs || s.mtime;
|
|
3763
|
-
return t && new Date(typeof t === 'number' ? t : t).getTime() >= todayStart.getTime();
|
|
4235
|
+
return t && new Date(typeof t === 'number' ? t : Number(t) || t).getTime() >= todayStart.getTime();
|
|
3764
4236
|
});
|
|
3765
4237
|
if (!todaySessions.length) return;
|
|
3766
4238
|
|
|
@@ -3779,8 +4251,8 @@ function buildDigest() {
|
|
|
3779
4251
|
const stats = [
|
|
3780
4252
|
`${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
|
|
3781
4253
|
totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
|
|
3782
|
-
totalTools > 0 ? `${totalTools} tool calls` : null,
|
|
3783
|
-
totalMsgs > 0 ? `${totalMsgs} messages` : null,
|
|
4254
|
+
totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
|
|
4255
|
+
totalMsgs > 0 ? `${totalMsgs.toLocaleString()} messages` : null,
|
|
3784
4256
|
longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
|
|
3785
4257
|
...themes.map(t => `#${t}`),
|
|
3786
4258
|
].filter(Boolean);
|
|
@@ -3792,7 +4264,7 @@ function buildDigest() {
|
|
|
3792
4264
|
const monthCostSoFar = allSessions.filter(s => {
|
|
3793
4265
|
const t = s.firstTs || s.mtime;
|
|
3794
4266
|
if (!t) return false;
|
|
3795
|
-
const d = new Date(typeof t === 'number' ? t : t);
|
|
4267
|
+
const d = new Date(typeof t === 'number' ? t : Number(t) || t);
|
|
3796
4268
|
return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
|
|
3797
4269
|
}).reduce((a, s) => a + (s.totalCost || 0), 0);
|
|
3798
4270
|
const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
|
|
@@ -3861,23 +4333,25 @@ function toggleLeaderboard() {
|
|
|
3861
4333
|
}
|
|
3862
4334
|
|
|
3863
4335
|
function renderLeaderboard() {
|
|
3864
|
-
const
|
|
4336
|
+
const all = [...allSessions]
|
|
3865
4337
|
.filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
|
|
3866
|
-
.sort((a, b) => b.totalCost - a.totalCost)
|
|
3867
|
-
|
|
4338
|
+
.sort((a, b) => b.totalCost - a.totalCost);
|
|
4339
|
+
const sorted = all.slice(0, 15);
|
|
3868
4340
|
const body = document.getElementById('lb-body');
|
|
3869
|
-
|
|
4341
|
+
const overflow = document.getElementById('lb-overflow');
|
|
4342
|
+
if (!sorted.length) { body.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-xs);padding:12px">No cost data yet</td></tr>'; if (overflow) overflow.textContent = ''; return; }
|
|
3870
4343
|
body.innerHTML = sorted.map((s, i) => {
|
|
3871
4344
|
const cost = '$' + s.totalCost.toFixed(2);
|
|
3872
4345
|
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
|
|
3873
4346
|
const prompt = s.lastPrompt || s.id;
|
|
3874
4347
|
return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
|
|
3875
4348
|
<td class="lb-rank">${i + 1}</td>
|
|
3876
|
-
<td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
|
|
4349
|
+
<td class="lb-prompt">${esc(prompt.slice(0, 60))}${prompt.length > 60 ? '…' : ''}</td>
|
|
3877
4350
|
<td class="lb-cost">${cost}</td>
|
|
3878
4351
|
<td class="lb-dur">${dur}</td>
|
|
3879
4352
|
</tr>`;
|
|
3880
4353
|
}).join('');
|
|
4354
|
+
if (overflow) overflow.textContent = all.length > 15 ? `Showing 15 of ${all.length} sessions` : '';
|
|
3881
4355
|
}
|
|
3882
4356
|
|
|
3883
4357
|
// ── feature 12: session diff ──────────────────────────────
|
|
@@ -3994,7 +4468,7 @@ async function exportSession() {
|
|
|
3994
4468
|
const events = data.events || [];
|
|
3995
4469
|
const lines = [
|
|
3996
4470
|
`# Session: ${sess.lastPrompt || sess.id}`,
|
|
3997
|
-
`> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`,
|
|
4471
|
+
`> ${new Date(typeof (sess.lastTs || sess.mtime) === 'number' ? (sess.lastTs || sess.mtime) : Number(sess.lastTs || sess.mtime) || (sess.lastTs || sess.mtime)).toLocaleString()}`,
|
|
3998
4472
|
sess.totalCost != null ? `> Cost: $${sess.totalCost.toFixed(2)}` : '',
|
|
3999
4473
|
sess.totalDurationMs ? `> Duration: ${fmtDur(sess.totalDurationMs)}` : '',
|
|
4000
4474
|
'',
|
|
@@ -4002,7 +4476,7 @@ async function exportSession() {
|
|
|
4002
4476
|
for (const ev of events) {
|
|
4003
4477
|
if (ev.kind === 'user' && ev.text?.trim()) {
|
|
4004
4478
|
lines.push(`\n## ${ev.text.trim().slice(0, 80)}`);
|
|
4005
|
-
if (ev.ts) lines.push(`_${new Date(ev.ts).toLocaleTimeString()}_`);
|
|
4479
|
+
if (ev.ts) lines.push(`_${new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleTimeString()}_`);
|
|
4006
4480
|
} else if (ev.kind === 'tool') {
|
|
4007
4481
|
const label = ev.label || ev.name || ev.cat;
|
|
4008
4482
|
lines.push(`- \`${ev.name || ev.cat}\`${label ? ': ' + label : ''}${ev._errored ? ' ⚠ error' : ''}`);
|
|
@@ -4078,9 +4552,9 @@ function renderBurnGauge() {
|
|
|
4078
4552
|
}
|
|
4079
4553
|
const now = Date.now();
|
|
4080
4554
|
// calls in last 5 min, 15 min, 60 min
|
|
4081
|
-
const t5 = tools.filter(e => now -
|
|
4082
|
-
const t15 = tools.filter(e => now -
|
|
4083
|
-
const t60 = tools.filter(e => now -
|
|
4555
|
+
const t5 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 300000).length;
|
|
4556
|
+
const t15 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 900000).length;
|
|
4557
|
+
const t60 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 3600000).length;
|
|
4084
4558
|
const rate5 = (t5 / 5).toFixed(1); // calls/min
|
|
4085
4559
|
const rate15 = (t15 / 15).toFixed(1);
|
|
4086
4560
|
const rate60 = (t60 / 60).toFixed(1);
|
|
@@ -4109,7 +4583,7 @@ function buildSwimlane() {
|
|
|
4109
4583
|
const LANE_HUES = [75, 200, 300, 150, 25, 220, 340, 120];
|
|
4110
4584
|
const rows = recent.map((s, si) => {
|
|
4111
4585
|
const start = s.firstTs || s.startTs || s.mtime || now;
|
|
4112
|
-
const startMs = typeof start === 'number' ? start : new Date(start).getTime();
|
|
4586
|
+
const startMs = typeof start === 'number' ? start : Number(start) || new Date(start).getTime() || 0;
|
|
4113
4587
|
const dur = s.totalDurationMs || 60000;
|
|
4114
4588
|
const endMs = startMs + dur;
|
|
4115
4589
|
const leftPct = Math.max(0, Math.min(100, ((startMs - windowStart) / windowMs) * 100));
|
|
@@ -4127,17 +4601,17 @@ function buildSwimlane() {
|
|
|
4127
4601
|
}).join('');
|
|
4128
4602
|
// dead time: find largest gap between consecutive sessions
|
|
4129
4603
|
const sorted = recent.slice().sort((a, b) => {
|
|
4130
|
-
const
|
|
4131
|
-
const
|
|
4604
|
+
const _aT = a.firstTs || a.mtime; const aTs = typeof _aT === 'number' ? _aT : Number(_aT) || new Date(_aT).getTime() || 0;
|
|
4605
|
+
const _bT = b.firstTs || b.mtime; const bTs = typeof _bT === 'number' ? _bT : Number(_bT) || new Date(_bT).getTime() || 0;
|
|
4132
4606
|
return aTs - bTs;
|
|
4133
4607
|
});
|
|
4134
4608
|
let maxGapMs = 0; let gapStart = 0;
|
|
4135
4609
|
for (let i = 1; i < sorted.length; i++) {
|
|
4136
4610
|
const prev = sorted[i - 1];
|
|
4137
4611
|
const curr = sorted[i];
|
|
4138
|
-
const
|
|
4612
|
+
const _pT = prev.firstTs || prev.mtime; const prevTs = typeof _pT === 'number' ? _pT : Number(_pT) || new Date(_pT).getTime() || 0;
|
|
4139
4613
|
const prevEnd = prevTs + (prev.totalDurationMs || 60000);
|
|
4140
|
-
const
|
|
4614
|
+
const _cT = curr.firstTs || curr.mtime; const currTs = typeof _cT === 'number' ? _cT : Number(_cT) || new Date(_cT).getTime() || 0;
|
|
4141
4615
|
const gap = currTs - prevEnd;
|
|
4142
4616
|
if (gap > maxGapMs) { maxGapMs = gap; gapStart = prevEnd; }
|
|
4143
4617
|
}
|
|
@@ -4171,6 +4645,33 @@ function buildLoopSparkline(l) {
|
|
|
4171
4645
|
return `<div class="le-spark"><span style="font-size:10px;color:var(--text-xs)">last ${runHistory.slice(-10).length} runs</span><div class="loop-sparkline">${bars}</div></div>`;
|
|
4172
4646
|
}
|
|
4173
4647
|
|
|
4648
|
+
function fmtInterval(v) {
|
|
4649
|
+
if (!v && v !== 0) return '';
|
|
4650
|
+
if (typeof v === 'string') return v;
|
|
4651
|
+
const m = parseInt(v);
|
|
4652
|
+
if (isNaN(m) || m <= 0) return String(v);
|
|
4653
|
+
if (m < 60) return m + 'm';
|
|
4654
|
+
if (m % 60 === 0) return (m / 60) + 'h';
|
|
4655
|
+
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
function fmtCountdown(nextAt) {
|
|
4659
|
+
const ms = parseInt(nextAt) - Date.now();
|
|
4660
|
+
if (ms <= 0) return 'overdue';
|
|
4661
|
+
const h = Math.floor(ms / 3600000);
|
|
4662
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
4663
|
+
const s = Math.floor((ms % 60000) / 1000);
|
|
4664
|
+
if (h > 0) return `next in ${h}h ${m}m`;
|
|
4665
|
+
return m > 0 ? `next in ${m}m ${s}s` : `next in ${s}s`;
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
function shortPath(p) {
|
|
4669
|
+
if (!p) return '—';
|
|
4670
|
+
const parts = p.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
4671
|
+
if (parts.length <= 2) return p;
|
|
4672
|
+
return '…/' + parts.slice(-2).join('/');
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4174
4675
|
// ── loops ──────────────────────────────────────────────────
|
|
4175
4676
|
async function renderLoops() {
|
|
4176
4677
|
const el = document.getElementById('loops-content');
|
|
@@ -4178,49 +4679,115 @@ async function renderLoops() {
|
|
|
4178
4679
|
try {
|
|
4179
4680
|
const data = await apiFetch('/api/loops?dir=' + enc(DIR));
|
|
4180
4681
|
const loops = Array.isArray(data) ? data : (data.loops || []);
|
|
4181
|
-
|
|
4682
|
+
const hilCountL = loops.filter(l => l.status === 'hil:pending').length;
|
|
4683
|
+
document.getElementById('bdg-loops').textContent = loops.length ? (hilCountL ? loops.length + '⚠' : loops.length) : '—';
|
|
4182
4684
|
if (!loops.length) {
|
|
4183
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops scheduled</div></div>';
|
|
4685
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops scheduled</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Create one with <code>+ New Loop</code> above or via <code>npx monomind autodev --tillend</code></div></div>';
|
|
4184
4686
|
return;
|
|
4185
4687
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
const
|
|
4198
|
-
const
|
|
4688
|
+
// Deduplicate: hide hook/lock entries shadowed by _repeat.md entries
|
|
4689
|
+
const hasRepeatLoops = loops.some(l => l.source === '_repeat.md');
|
|
4690
|
+
const repeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
|
|
4691
|
+
const dedupedLoops = loops.filter(l => {
|
|
4692
|
+
if (l.source === 'scheduled_tasks_lock' && hasRepeatLoops) return false;
|
|
4693
|
+
if (l.source !== 'schedule_wakeup_hook') return true;
|
|
4694
|
+
const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
|
|
4695
|
+
if (!m) return true;
|
|
4696
|
+
return !repeatPrompts.has(m[1].trim());
|
|
4697
|
+
});
|
|
4698
|
+
el.innerHTML = dedupedLoops.map((l, idx) => {
|
|
4699
|
+
const isHil = l.status === 'hil:pending';
|
|
4700
|
+
const isTillend = l.type === 'tillend';
|
|
4701
|
+
const curRep = l.currentRep || 0;
|
|
4702
|
+
const maxReps = l.maxReps || 0;
|
|
4703
|
+
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
|
|
4704
|
+
// Loops with status 'running'/'waiting'/'active' are explicitly active.
|
|
4705
|
+
// Don't mark them overdue unless nextRunAt is >2h stale (loop died without cleanup).
|
|
4706
|
+
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
4707
|
+
const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
|
|
4708
|
+
const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive &&
|
|
4709
|
+
nextAt > 0 && nextAt <= Date.now();
|
|
4710
|
+
const isStaledActive = isExplicitlyActive && nextAt > 0 &&
|
|
4711
|
+
(Date.now() - nextAt) > LOOP_STALE_MS;
|
|
4712
|
+
const isFinished = isOverdue || isStaledActive ||
|
|
4713
|
+
(!isExplicitlyActive && maxReps > 0 && curRep >= maxReps) ||
|
|
4714
|
+
l.status === 'finished' || l.status === 'done' ||
|
|
4715
|
+
l.status === 'complete' || l.status === 'completed' || l.status === 'expired';
|
|
4716
|
+
const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
|
|
4717
|
+
const intervalStr = fmtInterval(l.interval || l.schedule);
|
|
4718
|
+
// Parse loop into consistent: userPrompt / command / flags
|
|
4719
|
+
const _lp = (function(_l) {
|
|
4720
|
+
if (_l.command) {
|
|
4721
|
+
const flags = [];
|
|
4722
|
+
if (_l.type && _l.type !== 'repeat') flags.push('--' + _l.type);
|
|
4723
|
+
if (_l.maxReps) flags.push('--maxruns ' + _l.maxReps);
|
|
4724
|
+
if (_l.interval) { const s = String(_l.interval).match(/^(\d+)s$/); if (s) flags.push('--wait ' + s[1]); }
|
|
4725
|
+
if (_l.currentRep != null) flags.push('--rep ' + _l.currentRep);
|
|
4726
|
+
if (_l.id) flags.push('--loop ' + _l.id);
|
|
4727
|
+
return { userPrompt: _l.prompt || '', command: _l.command, flagsStr: flags.join(' ') };
|
|
4728
|
+
}
|
|
4729
|
+
const full = _l.prompt || '';
|
|
4730
|
+
const cmdM = full.match(/^(\/\S+)/);
|
|
4731
|
+
if (!cmdM) return { userPrompt: full, command: '', flagsStr: '' };
|
|
4732
|
+
const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
|
|
4733
|
+
let ti = 0, fp = [];
|
|
4734
|
+
while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
|
|
4735
|
+
fp.push(tokens[ti++]);
|
|
4736
|
+
if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) fp.push(tokens[ti++]);
|
|
4737
|
+
}
|
|
4738
|
+
return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1], flagsStr: fp.join(' ') };
|
|
4739
|
+
})(l);
|
|
4740
|
+
const userPrompt = _lp.userPrompt;
|
|
4741
|
+
const cmdStr = _lp.command;
|
|
4742
|
+
const flagsStr = _lp.flagsStr;
|
|
4743
|
+
const fullPrompt = l.prompt || '';
|
|
4744
|
+
const name = (l.name || userPrompt || cmdStr || 'loop').slice(0, 60);
|
|
4745
|
+
const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
|
|
4746
|
+
const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
|
|
4747
|
+
const pct = (!isTillend && maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
|
|
4748
|
+
const progBar = (!isTillend && maxReps > 0 && running)
|
|
4199
4749
|
? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
|
|
4200
4750
|
: '';
|
|
4201
|
-
const
|
|
4202
|
-
? `
|
|
4751
|
+
const runCountDisplay = isTillend
|
|
4752
|
+
? `run ${curRep} / ∞${maxReps > 0 ? ' (cap: ' + maxReps + ')' : ''}`
|
|
4753
|
+
: (maxReps > 0 ? `${curRep} / ${maxReps}` : String(curRep || '—'));
|
|
4754
|
+
const cdownSpan = nextAt
|
|
4755
|
+
? ` <span class="loop-cdown${nextAt - Date.now() <= 0 ? ' overdue' : ''}" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>`
|
|
4203
4756
|
: '';
|
|
4204
4757
|
const stopBtn = running
|
|
4205
|
-
? `<button class="loop-stop-btn" data-loop-id="${esc(l.id || l.name || String(idx))}" onclick="stopLoop(event, this.dataset.loopId)">■ Stop</button>`
|
|
4758
|
+
? `<button class="loop-stop-btn" data-loop-id="${esc(l.id || l.name || String(idx))}" onclick="stopLoop(event, this.dataset.loopId)" title="Stop this loop">■ Stop</button>`
|
|
4206
4759
|
: '';
|
|
4207
|
-
|
|
4208
|
-
|
|
4760
|
+
const typeBadge = `<span class="loop-type-badge${isTillend ? ' tillend' : ''}">${esc(l.type || 'repeat')}</span>`;
|
|
4761
|
+
const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
|
|
4762
|
+
const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
|
|
4763
|
+
const hilBanner = isHil
|
|
4764
|
+
? `<div class="loop-hil-banner">⚠ Waiting for human response — open HIL file to resume</div>`
|
|
4765
|
+
: '';
|
|
4766
|
+
const metaParts = [intervalStr, l.description].filter(Boolean).join(' · ').slice(0, 80);
|
|
4767
|
+
return `<div class="loop-row" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
|
|
4768
|
+
<div class="loop-ico">${isTillend ? '∞' : '↺'}</div>
|
|
4209
4769
|
<div class="loop-body">
|
|
4210
|
-
<div class="loop-name">${esc(name)}</div>
|
|
4211
|
-
<div class="loop-meta">${esc(
|
|
4770
|
+
<div class="loop-name" title="${esc(userPrompt || fullPrompt)}">${typeBadge}${esc(name)}</div>
|
|
4771
|
+
<div class="loop-meta">${esc(metaParts)}${cdownSpan}</div>
|
|
4772
|
+
${hilBanner}
|
|
4212
4773
|
${progBar}
|
|
4213
4774
|
</div>
|
|
4214
|
-
<div class="loop-status ${
|
|
4775
|
+
<div class="loop-status ${statusClass}">${statusLabel}</div>
|
|
4215
4776
|
${stopBtn}
|
|
4216
4777
|
</div>
|
|
4217
4778
|
<div class="loop-expand">
|
|
4218
|
-
${
|
|
4219
|
-
|
|
4220
|
-
|
|
4779
|
+
${userPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(userPrompt)}">${esc(userPrompt.slice(0, 300))}${userPrompt.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy prompt" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(userPrompt)}).then(()=>showToast('Copied','Prompt copied','ok'))">⎘</button></div></div>` : ''}
|
|
4780
|
+
${cmdStr ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="display:flex;align-items:center;gap:8px"><span>${esc(cmdStr)}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy command" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(cmdStr)}).then(()=>showToast('Copied','Command copied','ok'))">⎘</button></div></div>` : ''}
|
|
4781
|
+
${flagsStr ? `<div class="le-row"><div class="le-lbl">Flags</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(flagsStr)}">${esc(flagsStr.slice(0, 300))}${flagsStr.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy flags" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(flagsStr)}).then(()=>showToast('Copied','Flags copied','ok'))">⎘</button></div></div>` : ''}
|
|
4782
|
+
<div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${esc(l.type || 'repeat')}</div></div>
|
|
4783
|
+
<div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(intervalStr || '—')}</div></div>
|
|
4784
|
+
<div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isHil ? '⚠ hil:pending' : (running ? '● running' : (isFinished ? '✓ done' : '○ stopped'))}</div></div>
|
|
4785
|
+
${isHil && l.id ? `<div class="le-row"><div class="le-lbl">HIL file</div><div class="le-val mono" style="color:oklch(75% 0.16 60);word-break:break-all">.monomind/loops/${esc(l.id)}-hil.md</div></div>` : ''}
|
|
4221
4786
|
<div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
|
|
4222
|
-
<div class="le-row"><div class="le-lbl">
|
|
4223
|
-
<div class="le-row"><div class="le-lbl">
|
|
4787
|
+
${(()=>{ const sMs=l.startedAt?(typeof l.startedAt==='number'?l.startedAt:new Date(l.startedAt).getTime()):0; const age=sMs>0&&sMs<Date.now()?Date.now()-sMs:0; return age>0?`<div class="le-row"><div class="le-lbl">Running for</div><div class="le-val">${fmtDur(age)}</div></div>`:''; })()}
|
|
4788
|
+
<div class="le-row"><div class="le-lbl">Last run</div><div class="le-val" title="${l.lastRunAt ? new Date(typeof l.lastRunAt === 'number' ? l.lastRunAt : Number(l.lastRunAt) || l.lastRunAt).toLocaleString() : ''}">${esc(lastRun)}</div></div>
|
|
4789
|
+
<div class="le-row"><div class="le-lbl">${isTillend ? 'Progress' : 'Run count'}</div><div class="le-val">${esc(runCountDisplay)}</div></div>
|
|
4790
|
+
${l.source ? `<div class="le-row"><div class="le-lbl">Source</div><div class="le-val">${esc(l.source)}</div></div>` : ''}
|
|
4224
4791
|
${buildLoopSparkline(l)}
|
|
4225
4792
|
</div>`;
|
|
4226
4793
|
}).join('');
|
|
@@ -4257,7 +4824,20 @@ function hideLoopForm() {
|
|
|
4257
4824
|
|
|
4258
4825
|
async function stopLoop(evt, id) {
|
|
4259
4826
|
evt.stopPropagation();
|
|
4260
|
-
|
|
4827
|
+
const btn = evt.currentTarget;
|
|
4828
|
+
if (!btn.dataset.confirming) {
|
|
4829
|
+
btn.dataset.confirming = '1';
|
|
4830
|
+
btn.textContent = '■ Confirm?';
|
|
4831
|
+
btn.style.cssText = 'background:oklch(55% 0.2 25 / 0.25);color:oklch(72% 0.2 25);border-color:oklch(55% 0.2 25 / 0.4)';
|
|
4832
|
+
btn._resetTimer = setTimeout(() => {
|
|
4833
|
+
delete btn.dataset.confirming;
|
|
4834
|
+
btn.textContent = '■ Stop';
|
|
4835
|
+
btn.style.cssText = '';
|
|
4836
|
+
}, 3000);
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
clearTimeout(btn._resetTimer);
|
|
4840
|
+
delete btn.dataset.confirming;
|
|
4261
4841
|
try {
|
|
4262
4842
|
await fetch('/api/loops/stop?dir=' + enc(DIR), {
|
|
4263
4843
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
@@ -4276,12 +4856,30 @@ function startCountdowns() {
|
|
|
4276
4856
|
document.querySelectorAll('.loop-cdown[data-nextat]').forEach(el => {
|
|
4277
4857
|
const ms = parseInt(el.dataset.nextat) - Date.now();
|
|
4278
4858
|
if (ms <= 0) {
|
|
4279
|
-
|
|
4280
|
-
|
|
4859
|
+
const row = el.closest('.loop-row');
|
|
4860
|
+
const loopStatus = row ? (row.dataset.loopStatus || '') : '';
|
|
4861
|
+
const isActiveStatus = loopStatus === 'running' || loopStatus === 'waiting' || loopStatus === 'active';
|
|
4862
|
+
if (isActiveStatus) {
|
|
4863
|
+
// Loop is between rounds or executing — not done, just waiting for next update
|
|
4864
|
+
el.textContent = 'executing…';
|
|
4865
|
+
el.classList.remove('overdue');
|
|
4866
|
+
} else {
|
|
4867
|
+
el.textContent = 'overdue';
|
|
4868
|
+
el.classList.add('overdue');
|
|
4869
|
+
if (row) {
|
|
4870
|
+
const badge = row.querySelector('.loop-status');
|
|
4871
|
+
if (badge && badge.classList.contains('active')) {
|
|
4872
|
+
badge.classList.remove('active');
|
|
4873
|
+
badge.classList.add('stopped');
|
|
4874
|
+
badge.textContent = 'done';
|
|
4875
|
+
}
|
|
4876
|
+
const stopBtn = row.querySelector('.loop-stop-btn');
|
|
4877
|
+
if (stopBtn) stopBtn.remove();
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4281
4880
|
return;
|
|
4282
4881
|
}
|
|
4283
|
-
|
|
4284
|
-
el.textContent = m > 0 ? `next in ${m}m ${s}s` : `next in ${s}s`;
|
|
4882
|
+
el.textContent = fmtCountdown(el.dataset.nextat);
|
|
4285
4883
|
el.classList.remove('overdue');
|
|
4286
4884
|
});
|
|
4287
4885
|
}, 1000);
|
|
@@ -4334,7 +4932,7 @@ function buildEfficiencyPanel() {
|
|
|
4334
4932
|
<span class="eff-lbl" title="${esc(s.lastPrompt||s.id)}">${esc(lbl)}</span>
|
|
4335
4933
|
<span class="eff-pct ${cls}">${pct}%</span>
|
|
4336
4934
|
</div>
|
|
4337
|
-
<div class="eff-bar-wrap"><div class="eff-bar-fill" style="width:${pct}%;background:${fillColor}"></div></div>`;
|
|
4935
|
+
<div class="eff-bar-wrap" title="${esc(s.lastPrompt||s.id)}: ${pct}% cache efficiency"><div class="eff-bar-fill" style="width:${pct}%;background:${fillColor}"></div></div>`;
|
|
4338
4936
|
}).join('');
|
|
4339
4937
|
el.innerHTML = `<div class="m-group-title">Cache Efficiency <span class="${avgCls}" style="font-size:10px;font-weight:400">${avgPct}% avg</span></div>${rows}`;
|
|
4340
4938
|
}
|
|
@@ -4371,10 +4969,10 @@ function renderModelMix() {
|
|
|
4371
4969
|
const short = model.replace(/^claude-/,'').replace(/-\d{8}$/,'');
|
|
4372
4970
|
const pct = totalCost > 0 ? Math.round(d.cost/totalCost*100) : 0;
|
|
4373
4971
|
return `<tr>
|
|
4374
|
-
<td style="font-size:11px">${esc(short)}</td>
|
|
4972
|
+
<td style="font-size:11px" title="${esc(model)}">${esc(short)}</td>
|
|
4375
4973
|
<td class="lb-cost">$${d.cost.toFixed(2)}</td>
|
|
4376
4974
|
<td class="lb-dur">${pct}%</td>
|
|
4377
|
-
<td class="lb-dur">${d.calls}</td>
|
|
4975
|
+
<td class="lb-dur">${d.calls.toLocaleString()}</td>
|
|
4378
4976
|
</tr>`;
|
|
4379
4977
|
}).join('')}
|
|
4380
4978
|
</tbody></table>`;
|
|
@@ -4393,14 +4991,14 @@ function buildWeeklyRecap() {
|
|
|
4393
4991
|
const weekStart = new Date(); weekStart.setDate(weekStart.getDate() - weekStart.getDay()); weekStart.setHours(0,0,0,0);
|
|
4394
4992
|
const weekSess = allSessions.filter(s => {
|
|
4395
4993
|
const t = s.lastTs || s.mtime;
|
|
4396
|
-
return t && new Date(typeof t === 'number' ? t : t).getTime() >= weekStart.getTime();
|
|
4994
|
+
return t && new Date(typeof t === 'number' ? t : Number(t) || t).getTime() >= weekStart.getTime();
|
|
4397
4995
|
});
|
|
4398
4996
|
if (!weekSess.length) return;
|
|
4399
4997
|
const totalCost = weekSess.reduce((a,s) => a + (s.totalCost||0), 0);
|
|
4400
4998
|
const totalTools = weekSess.reduce((a,s) => a + (s.toolCalls||0), 0);
|
|
4401
4999
|
const days = new Set(weekSess.map(s => {
|
|
4402
5000
|
const t = s.lastTs || s.mtime;
|
|
4403
|
-
return new Date(typeof t === 'number' ? t : t).toDateString();
|
|
5001
|
+
return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString();
|
|
4404
5002
|
})).size;
|
|
4405
5003
|
const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
|
|
4406
5004
|
const streak = calcStreak();
|
|
@@ -4468,11 +5066,12 @@ function buildActivityHeatmap() {
|
|
|
4468
5066
|
for (const s of allSessions) {
|
|
4469
5067
|
const t = s.firstTs || s.mtime;
|
|
4470
5068
|
if (!t) continue;
|
|
4471
|
-
const d = new Date(typeof t === 'number' ? t : t);
|
|
5069
|
+
const d = new Date(typeof t === 'number' ? t : Number(t) || t);
|
|
4472
5070
|
grid[d.getDay()][d.getHours()]++;
|
|
4473
5071
|
}
|
|
4474
5072
|
const maxVal = Math.max(1, ...grid.flat());
|
|
4475
5073
|
const DAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];
|
|
5074
|
+
const FULL_DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
|
4476
5075
|
let html = '<div class="heatmap-grid">';
|
|
4477
5076
|
html += '<div class="heatmap-row"><div class="heatmap-lbl"></div>';
|
|
4478
5077
|
for (let h = 0; h < 24; h++) {
|
|
@@ -4485,7 +5084,7 @@ function buildActivityHeatmap() {
|
|
|
4485
5084
|
const v = grid[d][h];
|
|
4486
5085
|
const alpha = v > 0 ? Math.max(0.18, v/maxVal).toFixed(2) : 0;
|
|
4487
5086
|
const bg = v > 0 ? `oklch(65% 0.18 200 / ${alpha})` : 'transparent';
|
|
4488
|
-
html += `<div class="heatmap-cell" style="background:${bg};border:1px solid ${v>0?'transparent':'var(--border)'}" title="${
|
|
5087
|
+
html += `<div class="heatmap-cell" style="background:${bg};border:1px solid ${v>0?'transparent':'var(--border)'}" title="${FULL_DAYS[d]} ${h}:00–${h+1}:00 — ${v} session${v!==1?'s':''}"></div>`;
|
|
4489
5088
|
}
|
|
4490
5089
|
html += '</div>';
|
|
4491
5090
|
}
|
|
@@ -4567,8 +5166,8 @@ function v2RenderOrgList() {
|
|
|
4567
5166
|
return `<div class="org-item${active}" data-org="${esc(o.name)}" onclick="v2SelectOrg(this.dataset.org)">
|
|
4568
5167
|
<div class="oi-dot ${o.running ? 'running' : ''}"></div>
|
|
4569
5168
|
<div class="oi-body">
|
|
4570
|
-
<div class="oi-name">${esc(o.name)}</div>
|
|
4571
|
-
${goalSnip ? `<div class="oi-goal">${esc(goalSnip)}</div>` : ''}
|
|
5169
|
+
<div class="oi-name" title="${esc(o.name)}">${esc(o.name)}</div>
|
|
5170
|
+
${goalSnip ? `<div class="oi-goal" title="${esc(o.goal || goalSnip)}">${esc(goalSnip)}</div>` : ''}
|
|
4572
5171
|
<div class="oi-chips">
|
|
4573
5172
|
${o.running ? '<span class="oi-chip live">LIVE</span>' : ''}
|
|
4574
5173
|
<span class="oi-chip">${rolesN} roles</span>
|
|
@@ -5034,7 +5633,7 @@ function v2RenderOrgRoles() {
|
|
|
5034
5633
|
const pane = document.getElementById('odt-roles');
|
|
5035
5634
|
if (!pane) return;
|
|
5036
5635
|
if (!roles.length) {
|
|
5037
|
-
pane.innerHTML = '<div class="empty">No roles defined</div>';
|
|
5636
|
+
pane.innerHTML = '<div class="empty">No roles defined<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Roles are defined in the org config. Create an org with <code>npx monomind swarm init</code></div></div>';
|
|
5038
5637
|
return;
|
|
5039
5638
|
}
|
|
5040
5639
|
// Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
|
|
@@ -5106,7 +5705,7 @@ function v2RenderAgentDrawer(data) {
|
|
|
5106
5705
|
headEl.innerHTML = `
|
|
5107
5706
|
<img class="oad-avatar" src="${avatar}" alt="${esc(name)}" onerror="this.src='data/avatars/coder.svg'"/>
|
|
5108
5707
|
<div class="oad-id">
|
|
5109
|
-
<div class="oad-name">${esc(name)}</div>
|
|
5708
|
+
<div class="oad-name" title="${esc(name)}">${esc(name)}</div>
|
|
5110
5709
|
<div class="oad-pills">
|
|
5111
5710
|
${type ? `<span class="oad-pill">${esc(type)}</span>` : ''}
|
|
5112
5711
|
${model ? `<span class="oad-pill model">${esc(model)}</span>` : ''}
|
|
@@ -5184,7 +5783,7 @@ function v2RenderOrgActivity() {
|
|
|
5184
5783
|
if (!_v2SelOrg) return;
|
|
5185
5784
|
const activity = _v2OrgData?._activity || [];
|
|
5186
5785
|
const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
|
|
5187
|
-
const events = [...activity, ...orgEvents].sort((a,b) => (b.ts||0)-(a.ts||0)).slice(0,80);
|
|
5786
|
+
const events = [...activity, ...orgEvents].sort((a,b) => (Number(b.ts)||0)-(Number(a.ts)||0)).slice(0,80);
|
|
5188
5787
|
const pane = document.getElementById('odt-activity');
|
|
5189
5788
|
if (!pane) return;
|
|
5190
5789
|
const fmtOrgEvType = t => {
|
|
@@ -5192,13 +5791,14 @@ function v2RenderOrgActivity() {
|
|
|
5192
5791
|
return m[t]||(t||'').replace(/^org:/,'');
|
|
5193
5792
|
};
|
|
5194
5793
|
if (!events.length) {
|
|
5195
|
-
pane.innerHTML = '<div class="empty">No activity recorded</div>';
|
|
5794
|
+
pane.innerHTML = '<div class="empty">No activity recorded<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Activity is emitted when org agents run. Start an org with <code>npx monomind swarm start</code></div></div>';
|
|
5196
5795
|
return;
|
|
5197
5796
|
}
|
|
5198
5797
|
pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
|
|
5199
|
-
const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
5798
|
+
const t = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
5200
5799
|
const detail = ev.role||ev.msg||ev.agent||'';
|
|
5201
|
-
|
|
5800
|
+
const tFull = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '';
|
|
5801
|
+
return `<div class="av2-row"><span class="av2-time" title="${esc(tFull)}">${esc(t)}</span><span class="av2-type">${esc(fmtOrgEvType(ev.type))}</span><span class="av2-msg">${esc(detail)}</span></div>`;
|
|
5202
5802
|
}).join('')}</div>`;
|
|
5203
5803
|
}
|
|
5204
5804
|
|
|
@@ -5236,12 +5836,12 @@ function v2RenderOrgHeartbeats() {
|
|
|
5236
5836
|
status: a.status || 'idle',
|
|
5237
5837
|
}));
|
|
5238
5838
|
}
|
|
5239
|
-
if (!hb.length) { pane.innerHTML = '<div class="empty">No agents to report</div>'; return; }
|
|
5839
|
+
if (!hb.length) { pane.innerHTML = '<div class="empty">No agents to report<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Agent heartbeats appear when agents are running. Spawn one with <code>npx monomind agent spawn</code></div></div>'; return; }
|
|
5240
5840
|
const cls = (st) => (st === 'active' || st === 'running') ? 'on' : ((st === 'error' || st === 'failed') ? 'warn' : '');
|
|
5241
5841
|
pane.innerHTML = `<div class="m-group-title">Agent Heartbeats</div>` +
|
|
5242
5842
|
hb.slice(0, 50).map(h => `<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
|
|
5243
|
-
<span style="color:var(--text-hi);font-family:var(--mono);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(h.agent || '—')}</span>
|
|
5244
|
-
<span style="color:var(--text-lo);white-space:nowrap">${h.ts ? relTime(h.ts) : 'never'}</span>
|
|
5843
|
+
<span style="color:var(--text-hi);font-family:var(--mono);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(h.agent || '')}">${esc(h.agent || '—')}</span>
|
|
5844
|
+
<span style="color:var(--text-lo);white-space:nowrap" title="${h.ts ? new Date(typeof h.ts === 'number' ? h.ts : Number(h.ts) || h.ts).toLocaleString() : ''}">${h.ts ? relTime(h.ts) : 'never'}</span>
|
|
5245
5845
|
<span class="ss-pill ${cls(h.status)}">${esc(String(h.status || 'idle').toUpperCase())}</span>
|
|
5246
5846
|
</div>`).join('');
|
|
5247
5847
|
}
|
|
@@ -5260,15 +5860,15 @@ function v2RenderOrgTasks() {
|
|
|
5260
5860
|
(Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: t.status || col }));
|
|
5261
5861
|
}
|
|
5262
5862
|
}
|
|
5263
|
-
if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks</div>'; return; }
|
|
5863
|
+
if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Tasks are created automatically when agents run. Start one with <code>npx monomind agent spawn</code></div></div>'; return; }
|
|
5264
5864
|
const rank = { running: 0, doing: 0, todo: 1, pending: 1, done: 2 };
|
|
5265
5865
|
tasks.sort((a, b) => (rank[a.status] ?? 1) - (rank[b.status] ?? 1));
|
|
5266
5866
|
const pill = (st) => st === 'done' ? 'on' : (st === 'running' || st === 'doing' ? 'warn' : '');
|
|
5267
5867
|
pane.innerHTML = `<div class="m-group-title">Tasks (${tasks.length})</div>` +
|
|
5268
5868
|
tasks.slice(0, 80).map(t => `<div style="display:flex;gap:10px;align-items:baseline;padding:5px 0;border-bottom:1px solid var(--border);font-size:12px">
|
|
5269
5869
|
<span class="ss-pill ${pill(t.status)}">${esc(t.status || '?')}</span>
|
|
5270
|
-
<span style="flex:1;color:var(--text-hi);font-family:var(--mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.description || t.title || t.id || '—')}</span>
|
|
5271
|
-
<span style="color:var(--text-lo);white-space:nowrap">${relTime(t.ts || t.created_at || t.updated_at)}</span>
|
|
5870
|
+
<span style="flex:1;color:var(--text-hi);font-family:var(--mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.description || t.title || t.id || '')}">${esc(t.description || t.title || t.id || '—')}</span>
|
|
5871
|
+
<span style="color:var(--text-lo);white-space:nowrap" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(t.ts || t.created_at || t.updated_at)}">${relTime(t.ts || t.created_at || t.updated_at)}</span>
|
|
5272
5872
|
</div>`).join('');
|
|
5273
5873
|
}
|
|
5274
5874
|
|
|
@@ -5292,7 +5892,7 @@ function v2RenderOrgCosts() {
|
|
|
5292
5892
|
? c.map(r => ({ label: r.label ?? r.name ?? '—', cost: Number(r.value ?? r.cost ?? 0), tin: 0, tout: 0 }))
|
|
5293
5893
|
: Object.entries(c).map(([k, v]) => ({ label: k, cost: Number(v) || 0, tin: 0, tout: 0 }));
|
|
5294
5894
|
}
|
|
5295
|
-
if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data
|
|
5895
|
+
if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Cost tracking starts when agents run. Set a budget via the Org Settings tab.</div></div>'; return; }
|
|
5296
5896
|
const cur = (b && b.currency) || 'USD';
|
|
5297
5897
|
const period = (b && b.period) || '';
|
|
5298
5898
|
const total = rows.reduce((s, r) => s + r.cost, 0);
|
|
@@ -5304,7 +5904,7 @@ function v2RenderOrgCosts() {
|
|
|
5304
5904
|
<span style="color:var(--accent);font-family:var(--mono);font-size:15px;font-weight:600">$${total.toFixed(4)}</span>
|
|
5305
5905
|
</div>` +
|
|
5306
5906
|
rows.map(r => `<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
|
|
5307
|
-
<span style="color:var(--text-hi);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(String(r.label))}</span>
|
|
5907
|
+
<span style="color:var(--text-hi);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(String(r.label))}">${esc(String(r.label))}</span>
|
|
5308
5908
|
<span style="color:var(--text-lo);font-family:var(--mono);font-size:10px;white-space:nowrap">${(r.tin + r.tout).toLocaleString()} tok</span>
|
|
5309
5909
|
<span style="color:var(--accent);font-family:var(--mono);white-space:nowrap">$${r.cost.toFixed(4)}</span>
|
|
5310
5910
|
</div>`).join('') +
|
|
@@ -5321,14 +5921,14 @@ function v2RenderOrgMembers() {
|
|
|
5321
5921
|
const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
5322
5922
|
const list = members.length ? members : roles;
|
|
5323
5923
|
const joinReqs = Array.isArray(_v2OrgData?._joinRequests) ? _v2OrgData._joinRequests : [];
|
|
5324
|
-
if (!list.length) { pane.innerHTML = '<div class="empty">No members
|
|
5924
|
+
if (!list.length) { pane.innerHTML = '<div class="empty">No members<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Add agents or humans via <code>npx monomind agent spawn --org <name></code></div></div>'; return; }
|
|
5325
5925
|
const src = members.length ? 'joined members' : 'defined roles';
|
|
5326
5926
|
const active = (r) => r.running || r.active || r.status === 'active';
|
|
5327
5927
|
pane.innerHTML = `<div class="m-group-title">Members (${list.length}) · ${src}</div>` +
|
|
5328
5928
|
list.map(r => `<div style="display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
|
|
5329
5929
|
<span style="width:28px;height:28px;border-radius:50%;background:var(--surface-hi);display:inline-flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0">◈</span>
|
|
5330
5930
|
<div style="flex:1;min-width:0">
|
|
5331
|
-
<div style="color:var(--text-hi);font-family:var(--mono);font-size:12px">${esc(r.name || r.id || r.user || '—')}</div>
|
|
5931
|
+
<div style="color:var(--text-hi);font-family:var(--mono);font-size:12px" title="${esc(r.name || r.id || r.user || '')}">${esc(r.name || r.id || r.user || '—')}</div>
|
|
5332
5932
|
<div style="color:var(--text-lo);font-size:10px">${esc(r.title || r.type || r.role || '')}</div>
|
|
5333
5933
|
</div>
|
|
5334
5934
|
<span class="ss-pill ${active(r) ? 'on' : ''}">${active(r) ? 'ACTIVE' : 'IDLE'}</span>
|
|
@@ -5342,7 +5942,7 @@ function v2RenderOrgGoals() {
|
|
|
5342
5942
|
const el = document.getElementById('odt-goals');
|
|
5343
5943
|
if (!el || !_v2OrgData) return;
|
|
5344
5944
|
const goals = _v2OrgData.goals || _v2OrgData.config?.goals || [];
|
|
5345
|
-
if (!goals.length) { el.innerHTML = '<div class="empty">No goals defined</div>'; return; }
|
|
5945
|
+
if (!goals.length) { el.innerHTML = '<div class="empty">No goals defined<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Add goals to the org config via <code>npx monomind swarm config --goal "…"</code></div></div>'; return; }
|
|
5346
5946
|
function renderGoal(g, depth) {
|
|
5347
5947
|
if (depth > 20) return ''; // Depth guard to prevent stack overflow
|
|
5348
5948
|
const indent = depth * 20;
|
|
@@ -5366,7 +5966,7 @@ function v2RenderOrgBoard() {
|
|
|
5366
5966
|
const el = document.getElementById('odt-board');
|
|
5367
5967
|
if (!el || !_v2OrgData) return;
|
|
5368
5968
|
const issues = _v2OrgData._issues || [];
|
|
5369
|
-
if (!issues.length) { el.innerHTML = '<div class="empty">No issues
|
|
5969
|
+
if (!issues.length) { el.innerHTML = '<div class="empty">No issues<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Issues are created by agents during task execution or via the Issues tab.</div></div>'; return; }
|
|
5370
5970
|
const cols = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
5371
5971
|
const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
|
|
5372
5972
|
el.innerHTML = `<div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">` +
|
|
@@ -5375,9 +5975,10 @@ function v2RenderOrgBoard() {
|
|
|
5375
5975
|
return `<div style="min-width:160px;flex:1">
|
|
5376
5976
|
<div style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:var(--text-lo);padding:6px 0;border-bottom:1px solid var(--border);margin-bottom:8px">${esc(col.replace('_', ' '))} <span style="background:var(--surface-hi);padding:1px 6px;border-radius:8px">${cards.length}</span></div>
|
|
5377
5977
|
${cards.slice(0, 15).map(i => `<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:7px 9px;margin-bottom:5px;font-size:12px">
|
|
5378
|
-
<div style="color:var(--text-hi)">${PRIORITY[i.priority] || ''} ${esc((i.title || i.description || '—').slice(0, 60))}</div>
|
|
5978
|
+
<div style="color:var(--text-hi)" title="${esc(i.title || i.description || '')}">${PRIORITY[i.priority] || ''} ${esc((i.title || i.description || '—').slice(0, 60))}${(i.title || i.description || '').length > 60 ? '…' : ''}</div>
|
|
5379
5979
|
${i.assignee ? `<div style="font-size:10px;color:var(--text-lo);margin-top:3px">${esc(i.assignee)}</div>` : ''}
|
|
5380
5980
|
</div>`).join('')}
|
|
5981
|
+
${cards.length > 15 ? `<div style="font-size:11px;color:var(--text-xs);text-align:center;padding:4px 0">+${cards.length - 15} more</div>` : ''}
|
|
5381
5982
|
</div>`;
|
|
5382
5983
|
}).join('') + `</div>`;
|
|
5383
5984
|
}
|
|
@@ -5401,9 +6002,9 @@ function v2RenderOrgLive() {
|
|
|
5401
6002
|
<div class="m-group-title" style="margin-bottom:6px">Activity Feed</div>
|
|
5402
6003
|
<div style="max-height:240px;overflow-y:auto;font-size:11px;font-family:var(--mono)">
|
|
5403
6004
|
${(_v2OrgData._activity || []).slice(-30).reverse().map(e => `<div style="padding:3px 0;border-bottom:1px solid var(--border);color:var(--text-lo)">
|
|
5404
|
-
${esc(relTime(e.ts || e.timestamp || e.created_at))}
|
|
6005
|
+
<span title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(e.ts || e.timestamp || e.created_at)}">${esc(relTime(e.ts || e.timestamp || e.created_at))}</span>
|
|
5405
6006
|
<span style="color:var(--text-mid);margin-left:6px">${esc(e.type || e.kind || e.event || '—')}</span>
|
|
5406
|
-
${e.message ? `<span style="color:var(--text-hi);margin-left:6px">${esc(e.message.toString().slice(0, 80))}</span>` : ''}
|
|
6007
|
+
${e.message ? `<span style="color:var(--text-hi);margin-left:6px" title="${esc(e.message.toString())}">${esc(e.message.toString().slice(0, 80))}${e.message.toString().length > 80 ? '…' : ''}</span>` : ''}
|
|
5407
6008
|
</div>`).join('')}
|
|
5408
6009
|
</div>`;
|
|
5409
6010
|
// auto-refresh every 5s while tab is active
|
|
@@ -5428,7 +6029,7 @@ async function v2RenderOrgApprovals() {
|
|
|
5428
6029
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5429
6030
|
const data = await fetch(`/api/org/${_enc}/approvals${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
5430
6031
|
const approvals = Array.isArray(data) ? data : (data.approvals || []);
|
|
5431
|
-
if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals
|
|
6032
|
+
if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Approvals appear when agents request human confirmation before taking a sensitive action.</div></div>'; return; }
|
|
5432
6033
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5433
6034
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5434
6035
|
<th style="padding:6px 8px">Requester</th><th>Action</th><th>Status</th><th>Date</th><th></th>
|
|
@@ -5439,13 +6040,13 @@ async function v2RenderOrgApprovals() {
|
|
|
5439
6040
|
const aid = a.id || '';
|
|
5440
6041
|
return `<tr style="border-top:1px solid var(--border)">
|
|
5441
6042
|
<td style="padding:6px 8px;color:var(--text-hi)">${esc(a.requester || a.agent || '—')}</td>
|
|
5442
|
-
<td style="padding:6px 8px;color:var(--text-lo);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((a.action || a.description || '').slice(0, 80))}</td>
|
|
6043
|
+
<td style="padding:6px 8px;color:var(--text-lo);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(a.action || a.description || '')}">${esc((a.action || a.description || '').slice(0, 80))}${(a.action || a.description || '').length > 80 ? '…' : ''}</td>
|
|
5443
6044
|
<td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(a.status || 'pending')}</span></td>
|
|
5444
|
-
<td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
|
|
6045
|
+
<td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(a.created_at || a.ts)}">${relTime(a.created_at || a.ts)}</td>
|
|
5445
6046
|
<td style="padding:6px 8px;white-space:nowrap">
|
|
5446
6047
|
${pending
|
|
5447
|
-
? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" onclick="orgApprovalAction(${JSON.stringify(aid)},'approve')">✓</button>
|
|
5448
|
-
<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" onclick="orgApprovalAction(${JSON.stringify(aid)},'reject')">✕</button>`
|
|
6048
|
+
? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" onclick="orgApprovalAction(${JSON.stringify(aid).replace(/"/g, '"')},'approve')">✓</button>
|
|
6049
|
+
<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" onclick="orgApprovalAction(${JSON.stringify(aid).replace(/"/g, '"')},'reject')">✕</button>`
|
|
5449
6050
|
: ''}
|
|
5450
6051
|
</td>
|
|
5451
6052
|
</tr>`;
|
|
@@ -5481,7 +6082,7 @@ async function v2RenderOrgSecrets() {
|
|
|
5481
6082
|
${s.purpose ? `<span style="color:var(--text-lo)">${esc(s.purpose)}</span>` : ''}
|
|
5482
6083
|
<span style="margin-left:auto;font-family:var(--mono);color:var(--border)">••••••••</span>
|
|
5483
6084
|
</div>`).join('')
|
|
5484
|
-
: '<div class="empty">No secrets configured</div>');
|
|
6085
|
+
: '<div class="empty">No secrets configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Add secrets to the org config under <code>secrets: []</code> — values are never stored in the dashboard.</div></div>');
|
|
5485
6086
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
5486
6087
|
}
|
|
5487
6088
|
|
|
@@ -5498,19 +6099,19 @@ function v2RenderOrgSettings() {
|
|
|
5498
6099
|
el.innerHTML = `
|
|
5499
6100
|
<div style="font-size:11px;color:var(--text-lo);margin-bottom:14px">Changes generate a CLI command — no direct writes from UI.</div>
|
|
5500
6101
|
<div style="display:flex;flex-direction:column;gap:12px;max-width:380px">
|
|
5501
|
-
<div><div class="le-lbl">Goal</div><input id="os-goal" class="filter-input" value="${esc(d.goal || '')}"></div>
|
|
6102
|
+
<div><div class="le-lbl">Goal</div><input id="os-goal" class="filter-input" placeholder="Describe the org's mission…" value="${esc(d.goal || '')}"></div>
|
|
5502
6103
|
<div><div class="le-lbl">Topology</div>
|
|
5503
|
-
<select id="os-topo" class="filter-input" style="cursor:pointer">
|
|
6104
|
+
<select id="os-topo" class="filter-input" title="Agent coordination topology" style="cursor:pointer">
|
|
5504
6105
|
${topos.map(t => `<option ${d.topology === t ? 'selected' : ''}>${esc(t)}</option>`).join('')}
|
|
5505
6106
|
</select>
|
|
5506
6107
|
</div>
|
|
5507
6108
|
<div><div class="le-lbl">Governance</div>
|
|
5508
|
-
<select id="os-gov" class="filter-input" style="cursor:pointer">
|
|
6109
|
+
<select id="os-gov" class="filter-input" title="Governance / consensus strategy" style="cursor:pointer">
|
|
5509
6110
|
${govs.map(g => `<option ${govVal === g ? 'selected' : ''}>${esc(g)}</option>`).join('')}
|
|
5510
6111
|
</select>
|
|
5511
6112
|
</div>
|
|
5512
|
-
<div><div class="le-lbl">Budget (tokens)</div><input id="os-budget" class="filter-input" type="number" value="${esc(String(budgetVal || ''))}"></div>
|
|
5513
|
-
<button class="btn" style="width:fit-content;color:var(--accent);border-color:var(--accent)" onclick="generateOrgSettingsCmd()">Generate CLI Command</button>
|
|
6113
|
+
<div><div class="le-lbl">Budget (tokens)</div><input id="os-budget" class="filter-input" type="number" placeholder="e.g. 100000" value="${esc(String(budgetVal || ''))}"></div>
|
|
6114
|
+
<button class="btn" title="Generate a monomind CLI command from these settings" style="width:fit-content;color:var(--accent);border-color:var(--accent)" onclick="generateOrgSettingsCmd()">Generate CLI Command</button>
|
|
5514
6115
|
<div id="os-cmd-out" style="display:none;font-family:var(--mono);font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px;word-break:break-all;cursor:pointer;color:var(--text-hi)" title="Click to copy" onclick="navigator.clipboard.writeText(this.textContent).then(()=>showToast('Copied','','ok'))"></div>
|
|
5515
6116
|
</div>`;
|
|
5516
6117
|
}
|
|
@@ -5540,7 +6141,7 @@ async function v2RenderOrgRoutines() {
|
|
|
5540
6141
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5541
6142
|
const data = await fetch(`/api/org/${_enc}/routines${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
5542
6143
|
const routines = Array.isArray(data) ? data : (data.routines || _v2OrgData?.config?.routines || []);
|
|
5543
|
-
if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured</div>'; return; }
|
|
6144
|
+
if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Routines are scheduled agent tasks. Add them to the org config under <code>routines: []</code></div></div>'; return; }
|
|
5544
6145
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5545
6146
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5546
6147
|
<th style="padding:5px 8px">Name</th><th>Cron</th><th>Last Run</th><th>Status</th>
|
|
@@ -5548,7 +6149,7 @@ async function v2RenderOrgRoutines() {
|
|
|
5548
6149
|
routines.map(r => `<tr style="border-top:1px solid var(--border)">
|
|
5549
6150
|
<td style="padding:6px 8px;color:var(--text-hi)">${esc(r.name || '—')}</td>
|
|
5550
6151
|
<td style="padding:6px 8px;font-family:var(--mono);color:var(--text-lo)">${esc(r.cron || r.schedule || '—')}</td>
|
|
5551
|
-
<td style="padding:6px 8px;color:var(--text-lo)">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
|
|
6152
|
+
<td style="padding:6px 8px;color:var(--text-lo)" title="${r.lastRun ? new Date(typeof r.lastRun === 'number' ? r.lastRun : Number(r.lastRun) || r.lastRun).toLocaleString() : ''}">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
|
|
5552
6153
|
<td style="padding:6px 8px"><span class="ss-pill ${r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || '—')}</span></td>
|
|
5553
6154
|
</tr>`).join('') + '</tbody></table>';
|
|
5554
6155
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
@@ -5563,7 +6164,7 @@ async function v2RenderOrgMyIssues() {
|
|
|
5563
6164
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5564
6165
|
const data = await fetch(`/api/org/${_enc}/my-issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
5565
6166
|
const issues = Array.isArray(data) ? data : (data.issues || []);
|
|
5566
|
-
if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you
|
|
6167
|
+
if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Issues are assigned by agents or via the Board tab. Check the Issues tab to see all open issues.</div></div>'; return; }
|
|
5567
6168
|
const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
|
|
5568
6169
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5569
6170
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
@@ -5573,9 +6174,9 @@ async function v2RenderOrgMyIssues() {
|
|
|
5573
6174
|
const cls = i.status === 'done' ? 'on' : i.status === 'blocked' ? 'warn' : '';
|
|
5574
6175
|
return `<tr style="border-top:1px solid var(--border)">
|
|
5575
6176
|
<td style="padding:5px 4px">${PRIORITY[i.priority] || '·'}</td>
|
|
5576
|
-
<td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((i.title || i.description || '—').slice(0, 80))}</td>
|
|
6177
|
+
<td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(i.title || i.description || '')}">${esc((i.title || i.description || '—').slice(0, 80))}${(i.title || i.description || '').length > 80 ? '…' : ''}</td>
|
|
5577
6178
|
<td style="padding:5px 8px"><span class="ss-pill ${cls}">${esc(i.status || 'open')}</span></td>
|
|
5578
|
-
<td style="padding:5px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(i.updated_at || i.ts)}</td>
|
|
6179
|
+
<td style="padding:5px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(i.updated_at || i.ts)}">${relTime(i.updated_at || i.ts)}</td>
|
|
5579
6180
|
</tr>`;
|
|
5580
6181
|
}).join('') + '</tbody></table>';
|
|
5581
6182
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
@@ -5604,19 +6205,21 @@ function v2RenderOrgBudgets() {
|
|
|
5604
6205
|
html += `<div class="m-group-title">USD Budget</div>${fillBar(b.usd || 0, b.usdLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">$${Number(b.usd || 0).toFixed(4)} / ${b.usdLimit ? '$' + Number(b.usdLimit).toFixed(2) : '∞'}</div>`;
|
|
5605
6206
|
}
|
|
5606
6207
|
if (agents.length) {
|
|
6208
|
+
const shownAgents = agents.slice(0, 20);
|
|
5607
6209
|
html += '<div class="m-group-title" style="margin-bottom:6px">Per Agent</div>' +
|
|
5608
6210
|
`<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="color:var(--text-xs);text-align:left"><th style="padding:4px 8px">Agent</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th></tr></thead><tbody>` +
|
|
5609
|
-
|
|
6211
|
+
shownAgents.map(a => {
|
|
5610
6212
|
const over = a.budgetLimit && ((a.tokensIn || 0) + (a.tokensOut || 0)) > a.budgetLimit;
|
|
5611
6213
|
return `<tr style="border-top:1px solid var(--border)${over ? ';color:var(--red)' : ''}">
|
|
5612
|
-
<td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}">${esc((a.id || a.title || '—').toString().slice(0, 14))}</td>
|
|
6214
|
+
<td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}" title="${esc((a.id || a.title || '').toString())}">${esc((a.id || a.title || '—').toString().slice(0, 14))}${(a.id || a.title || '').toString().length > 14 ? '…' : ''}</td>
|
|
5613
6215
|
<td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensIn || 0).toLocaleString()}</td>
|
|
5614
6216
|
<td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensOut || 0).toLocaleString()}</td>
|
|
5615
6217
|
<td style="padding:4px 8px;color:var(--accent)">$${Number(a.cost || 0).toFixed(4)}${over ? ' ⚠' : ''}</td>
|
|
5616
6218
|
</tr>`;
|
|
5617
|
-
}).join('') + '</tbody></table>'
|
|
6219
|
+
}).join('') + '</tbody></table>' +
|
|
6220
|
+
(agents.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${agents.length} agents</div>` : '');
|
|
5618
6221
|
}
|
|
5619
|
-
el.innerHTML = html || '<div class="empty">No budget data
|
|
6222
|
+
el.innerHTML = html || '<div class="empty">No budget data<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Set token budgets in Org Settings to track spend per agent.</div></div>';
|
|
5620
6223
|
}
|
|
5621
6224
|
|
|
5622
6225
|
// ── PLUGINS ────────────────────────────────────────────────
|
|
@@ -5628,14 +6231,14 @@ async function v2RenderOrgPlugins() {
|
|
|
5628
6231
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5629
6232
|
const data = await fetch(`/api/org/${_enc}/plugins${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
5630
6233
|
const plugins = Array.isArray(data) ? data : (data.plugins || []);
|
|
5631
|
-
if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
|
|
6234
|
+
if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Browse and install plugins with <code>npx monomind plugins list</code></div></div>'; return; }
|
|
5632
6235
|
el.innerHTML = `<div class="proj-grid">` +
|
|
5633
6236
|
plugins.map(p => {
|
|
5634
6237
|
const status = p.status || 'installed';
|
|
5635
6238
|
const col = status === 'error' ? 'var(--red)' : status === 'installed' ? 'var(--accent)' : 'var(--text-lo)';
|
|
5636
6239
|
return `<div class="proj-card">
|
|
5637
|
-
<div class="proj-card-name">${esc(p.name || '—')}</div>
|
|
5638
|
-
<div class="proj-card-path">${esc((p.description || '').slice(0, 80))}</div>
|
|
6240
|
+
<div class="proj-card-name" title="${esc(p.name || '')}">${esc(p.name || '—')}</div>
|
|
6241
|
+
<div class="proj-card-path" title="${esc(p.description || '')}">${esc((p.description || '').slice(0, 80))}${(p.description || '').length > 80 ? '…' : ''}</div>
|
|
5639
6242
|
<div style="margin-top:8px"><span class="ss-pill" style="color:${esc(col)};border-color:${esc(col)}44;background:${esc(col)}18">${esc(status)}</span></div>
|
|
5640
6243
|
</div>`;
|
|
5641
6244
|
}).join('') + `</div>`;
|
|
@@ -5658,7 +6261,7 @@ function v2RenderOrgCharts() {
|
|
|
5658
6261
|
events: 0, errors: 0,
|
|
5659
6262
|
}));
|
|
5660
6263
|
activity.forEach(e => {
|
|
5661
|
-
const
|
|
6264
|
+
const _et = e.ts || e.timestamp || e.created_at || 0; const ts = typeof _et === 'number' ? _et : Number(_et) || new Date(_et).getTime() || 0;
|
|
5662
6265
|
const idx = buckets.findIndex((b, i) =>
|
|
5663
6266
|
ts >= b.ts && (i === 13 || ts < buckets[i + 1].ts));
|
|
5664
6267
|
if (idx >= 0) {
|
|
@@ -5683,7 +6286,7 @@ function v2RenderOrgCharts() {
|
|
|
5683
6286
|
// Per-agent run bars
|
|
5684
6287
|
const agentRuns = agents.slice(0, 10).map(a => {
|
|
5685
6288
|
const runs = activity.filter(e => e.agentId === a.id || e.agent === a.id).length;
|
|
5686
|
-
return { name: (a.type || a.title || a.id || '—').toString()
|
|
6289
|
+
return { name: (a.type || a.title || a.id || '—').toString(), runs };
|
|
5687
6290
|
}).filter(a => a.runs > 0).sort((x, y) => y.runs - x.runs);
|
|
5688
6291
|
const maxRuns = Math.max(...agentRuns.map(a => a.runs), 1);
|
|
5689
6292
|
|
|
@@ -5703,7 +6306,7 @@ function v2RenderOrgCharts() {
|
|
|
5703
6306
|
<div style="display:flex;gap:2px;align-items:flex-end;padding-bottom:28px;margin-bottom:16px;overflow-x:auto">${heatmap}</div>
|
|
5704
6307
|
${agentRuns.length ? `<div class="m-group-title" style="margin-bottom:6px">Per-Agent Runs</div>
|
|
5705
6308
|
${agentRuns.map(a => `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">
|
|
5706
|
-
<div style="width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px">${esc(a.name)}</div>
|
|
6309
|
+
<div style="width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc(a.name)}">${esc(a.name.slice(0, 20))}${a.name.length > 20 ? '…' : ''}</div>
|
|
5707
6310
|
<div style="flex:1;height:7px;background:var(--border);border-radius:2px;overflow:hidden"><div style="width:${Math.round(a.runs/maxRuns*100)}%;height:100%;background:var(--accent);border-radius:2px"></div></div>
|
|
5708
6311
|
<div style="width:30px;text-align:right;color:var(--text-lo);font-family:var(--mono);font-size:11px">${a.runs}</div>
|
|
5709
6312
|
</div>`).join('')}` : ''}
|
|
@@ -5719,10 +6322,10 @@ async function v2RenderOrgProjects() {
|
|
|
5719
6322
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5720
6323
|
const data = await fetch(`/api/org/${_enc}/projects${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
5721
6324
|
const projects = Array.isArray(data) ? data : (data.projects || []);
|
|
5722
|
-
if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured</div>'; return; }
|
|
6325
|
+
if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Run <code>npx monomind init</code> inside a project to add it.</div></div>'; return; }
|
|
5723
6326
|
el.innerHTML = `<div class="proj-grid">${projects.map(p => `<div class="proj-card">
|
|
5724
|
-
<div class="proj-card-name">${esc(p.name || p.title || '—')}</div>
|
|
5725
|
-
<div class="proj-card-path">${esc((p.description || p.path || '').slice(0,80))}</div>
|
|
6327
|
+
<div class="proj-card-name" title="${esc(p.name || p.title || '')}">${esc(p.name || p.title || '—')}</div>
|
|
6328
|
+
<div class="proj-card-path" title="${esc(p.description || p.path || '')}">${esc((p.description || p.path || '').slice(0,80))}${(p.description || p.path || '').length > 80 ? '…' : ''}</div>
|
|
5726
6329
|
<div class="proj-card-stats" style="margin-top:8px">
|
|
5727
6330
|
${p.status ? `<span class="ss-pill ${p.status==='active'?'on':''}">${esc(p.status)}</span>` : ''}
|
|
5728
6331
|
</div>
|
|
@@ -5735,7 +6338,7 @@ async function v2RenderOrgSkills() {
|
|
|
5735
6338
|
const el = document.getElementById('odt-skills');
|
|
5736
6339
|
if (!el || !_v2OrgData) return;
|
|
5737
6340
|
const roles = Array.isArray(_v2OrgData.roles) ? _v2OrgData.roles : [];
|
|
5738
|
-
if (!roles.length) { el.innerHTML = '<div class="empty">No roles defined</div>'; return; }
|
|
6341
|
+
if (!roles.length) { el.innerHTML = '<div class="empty">No roles defined<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Skills come from agent roles. Create an org with <code>npx monomind swarm init</code></div></div>'; return; }
|
|
5739
6342
|
el.innerHTML = '<div class="empty">Loading skills…</div>';
|
|
5740
6343
|
const org = _v2SelOrg, dirq = DIR ? '?dir=' + encodeURIComponent(DIR) : '';
|
|
5741
6344
|
// Enrich each role with expertise + task types from its agent definition (same source as the detail drawer)
|
|
@@ -5774,7 +6377,7 @@ async function v2RenderOrgWorkspaces() {
|
|
|
5774
6377
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5775
6378
|
const data = await fetch(`/api/org/${_enc}/workspaces${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
5776
6379
|
const ws = Array.isArray(data) ? data : (data.workspaces || []);
|
|
5777
|
-
if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured
|
|
6380
|
+
if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Workspaces are project directories added to this org. Add one via Org Settings.</div></div>'; return; }
|
|
5778
6381
|
el.innerHTML = ws.map(w => `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
|
|
5779
6382
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
|
5780
6383
|
<span style="font-size:13px;color:var(--text-hi);font-weight:500">${esc(w.name || w.id || '—')}</span>
|
|
@@ -5795,16 +6398,16 @@ async function v2RenderOrgInvites() {
|
|
|
5795
6398
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5796
6399
|
const data = await fetch(`/api/org/${_enc}/invites${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
5797
6400
|
const invites = Array.isArray(data) ? data : (data.invites || []);
|
|
5798
|
-
if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens
|
|
6401
|
+
if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Generate an invite token to add members to this org.</div></div>'; return; }
|
|
5799
6402
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5800
6403
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5801
6404
|
<th style="padding:5px 8px">Token</th><th>Role</th><th>Expires</th><th>Created</th>
|
|
5802
6405
|
</tr></thead>
|
|
5803
6406
|
<tbody>${invites.map(i => `<tr style="border-top:1px solid var(--border)">
|
|
5804
|
-
<td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px">${esc((i.token||i.id||'—').slice(0,16))}…</td>
|
|
6407
|
+
<td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc(i.token||i.id||'')}">${esc((i.token||i.id||'—').slice(0,16))}…</td>
|
|
5805
6408
|
<td style="padding:5px 8px"><span class="ss-pill">${esc(i.role||'viewer')}</span></td>
|
|
5806
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${i.expiresAt ? relTime(i.expiresAt) : '—'}</td>
|
|
5807
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${i.createdAt ? relTime(i.createdAt) : '—'}</td>
|
|
6409
|
+
<td style="padding:5px 8px;color:var(--text-lo)" title="${i.expiresAt ? new Date(typeof i.expiresAt === 'number' ? i.expiresAt : Number(i.expiresAt) || i.expiresAt).toLocaleString() : ''}">${i.expiresAt ? relTime(i.expiresAt) : '—'}</td>
|
|
6410
|
+
<td style="padding:5px 8px;color:var(--text-lo)" title="${i.createdAt ? new Date(typeof i.createdAt === 'number' ? i.createdAt : Number(i.createdAt) || i.createdAt).toLocaleString() : ''}">${i.createdAt ? relTime(i.createdAt) : '—'}</td>
|
|
5808
6411
|
</tr>`).join('')}</tbody>
|
|
5809
6412
|
</table>`;
|
|
5810
6413
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
@@ -5815,17 +6418,17 @@ function v2RenderOrgAgentsFull() {
|
|
|
5815
6418
|
const el = document.getElementById('odt-agents-full');
|
|
5816
6419
|
if (!el || !_v2OrgData) return;
|
|
5817
6420
|
const agents = _v2OrgData._agents || [];
|
|
5818
|
-
if (!agents.length) { el.innerHTML = '<div class="empty">No agents found
|
|
6421
|
+
if (!agents.length) { el.innerHTML = '<div class="empty">No agents found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Agents join this org when spawned with <code>npx monomind agent spawn --org <name></code></div></div>'; return; }
|
|
5819
6422
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5820
6423
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5821
6424
|
<th style="padding:5px 8px">ID</th><th>Type</th><th>Adapter</th><th>Status</th><th>Heartbeat</th><th>Tokens In</th><th>Tokens Out</th>
|
|
5822
6425
|
</tr></thead>
|
|
5823
6426
|
<tbody>${agents.map(a => `<tr style="border-top:1px solid var(--border)">
|
|
5824
|
-
<td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px">${esc((a.id||'—').toString().slice(0,12))}</td>
|
|
6427
|
+
<td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc((a.id||'').toString())}">${esc((a.id||'—').toString().slice(0,12))}${(a.id||'').toString().length > 12 ? '…' : ''}</td>
|
|
5825
6428
|
<td style="padding:5px 8px;color:var(--text-hi)">${esc(a.type||a.title||'—')}</td>
|
|
5826
6429
|
<td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapter||'—')}</td>
|
|
5827
6430
|
<td style="padding:5px 8px"><span class="ss-pill ${(a.status==='running'||a.running)?'on':''}">${esc(a.status||'idle')}</span></td>
|
|
5828
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
|
|
6431
|
+
<td style="padding:5px 8px;color:var(--text-lo)" title="${a.lastHeartbeat ? new Date(typeof a.lastHeartbeat === 'number' ? a.lastHeartbeat : Number(a.lastHeartbeat) || a.lastHeartbeat).toLocaleString() : ''}">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
|
|
5829
6432
|
<td style="padding:5px 8px;color:var(--text-lo)">${a.tokensIn != null ? Number(a.tokensIn).toLocaleString() : '—'}</td>
|
|
5830
6433
|
<td style="padding:5px 8px;color:var(--text-lo)">${a.tokensOut != null ? Number(a.tokensOut).toLocaleString() : '—'}</td>
|
|
5831
6434
|
</tr>`).join('')}</tbody>
|
|
@@ -5841,7 +6444,7 @@ async function v2RenderOrgEnvironments() {
|
|
|
5841
6444
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5842
6445
|
const data = await fetch(`/api/org/${_enc}/environments${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
5843
6446
|
const envs = Array.isArray(data) ? data : (data.environments || []);
|
|
5844
|
-
if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured</div>'; return; }
|
|
6447
|
+
if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Environments (dev/staging/prod) are declared in the org config under <code>environments: []</code></div></div>'; return; }
|
|
5845
6448
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5846
6449
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5847
6450
|
<th style="padding:5px 8px">Name</th><th>Driver</th><th>Host</th><th>Status</th>
|
|
@@ -5861,7 +6464,7 @@ function v2RenderOrgAccess() {
|
|
|
5861
6464
|
const el = document.getElementById('odt-access');
|
|
5862
6465
|
if (!el || !_v2OrgData) return;
|
|
5863
6466
|
const members = _v2OrgData._members || [];
|
|
5864
|
-
if (!members.length) { el.innerHTML = '<div class="empty">No members found
|
|
6467
|
+
if (!members.length) { el.innerHTML = '<div class="empty">No members found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Members are added when agents join the org. See the Members tab to manage roles.</div></div>'; return; }
|
|
5865
6468
|
const TIER = { owner: 0, admin: 1, operator: 2, viewer: 3 };
|
|
5866
6469
|
const byRole = {};
|
|
5867
6470
|
members.forEach(m => { const r = m.role || 'viewer'; (byRole[r] || (byRole[r] = [])).push(m); });
|
|
@@ -5883,7 +6486,7 @@ function v2RenderOrgIssuesFull() {
|
|
|
5883
6486
|
const el = document.getElementById('odt-issues-full');
|
|
5884
6487
|
if (!el || !_v2OrgData) return;
|
|
5885
6488
|
const issues = _v2OrgData._issues || [];
|
|
5886
|
-
if (!issues.length) { el.innerHTML = '<div class="empty">No issues found
|
|
6489
|
+
if (!issues.length) { el.innerHTML = '<div class="empty">No issues found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Issues are created by agents during task execution. Check the Board for status.</div></div>'; return; }
|
|
5887
6490
|
const PRIORITY = { urgent:'🔴', high:'🟠', medium:'🟡', low:'🟢' };
|
|
5888
6491
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5889
6492
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
@@ -5891,10 +6494,10 @@ function v2RenderOrgIssuesFull() {
|
|
|
5891
6494
|
</tr></thead>
|
|
5892
6495
|
<tbody>${issues.slice(0,100).map(i => `<tr style="border-top:1px solid var(--border)">
|
|
5893
6496
|
<td style="padding:5px 4px">${PRIORITY[i.priority]||'·'}</td>
|
|
5894
|
-
<td style="padding:5px 8px;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((i.title||i.description||'—').slice(0,80))}</td>
|
|
6497
|
+
<td style="padding:5px 8px;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(i.title||i.description||'')}">${esc((i.title||i.description||'—').slice(0,80))}${(i.title||i.description||'').length > 80 ? '…' : ''}</td>
|
|
5895
6498
|
<td style="padding:5px 8px;color:var(--text-lo)">${esc(i.assignee||'—')}</td>
|
|
5896
6499
|
<td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'?'on':i.status==='blocked'?'warn':''}">${esc(i.status||'open')}</span></td>
|
|
5897
|
-
<td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px">${relTime(i.updated_at||i.ts)}</td>
|
|
6500
|
+
<td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(i.updated_at||i.ts)}">${relTime(i.updated_at||i.ts)}</td>
|
|
5898
6501
|
</tr>`).join('')}</tbody>
|
|
5899
6502
|
</table>`;
|
|
5900
6503
|
}
|
|
@@ -5908,7 +6511,7 @@ async function v2RenderOrgJoinRequests() {
|
|
|
5908
6511
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5909
6512
|
const data = await fetch(`/api/org/${_enc}/join-requests${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
5910
6513
|
const reqs = Array.isArray(data) ? data : (data.joinRequests || data.join_requests || []);
|
|
5911
|
-
if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests
|
|
6514
|
+
if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Join requests appear when agents or users request access to this org.</div></div>'; return; }
|
|
5912
6515
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5913
6516
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5914
6517
|
<th style="padding:5px 8px">Requester</th><th>Type</th><th>Status</th><th>Created</th>
|
|
@@ -5917,7 +6520,7 @@ async function v2RenderOrgJoinRequests() {
|
|
|
5917
6520
|
<td style="padding:5px 8px;color:var(--text-hi)">${esc(r.name||r.username||r.id||'—')}</td>
|
|
5918
6521
|
<td style="padding:5px 8px;color:var(--text-lo)">${r.type==='agent'?'🤖 agent':'👤 human'}</td>
|
|
5919
6522
|
<td style="padding:5px 8px"><span class="ss-pill ${r.status==='approved'?'on':r.status==='rejected'?'warn':''}">${esc(r.status||'pending')}</span></td>
|
|
5920
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${r.createdAt ? relTime(r.createdAt) : '—'}</td>
|
|
6523
|
+
<td style="padding:5px 8px;color:var(--text-lo)" title="${r.createdAt ? new Date(typeof r.createdAt === 'number' ? r.createdAt : Number(r.createdAt) || r.createdAt).toLocaleString() : ''}">${r.createdAt ? relTime(r.createdAt) : '—'}</td>
|
|
5921
6524
|
</tr>`).join('')}</tbody>
|
|
5922
6525
|
</table>`;
|
|
5923
6526
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
@@ -5932,17 +6535,17 @@ async function v2RenderOrgThreads() {
|
|
|
5932
6535
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5933
6536
|
const data = await fetch(`/api/org/${_enc}/threads${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
5934
6537
|
const threads = Array.isArray(data) ? data : (data.threads || []);
|
|
5935
|
-
if (!threads.length) { el.innerHTML = '<div class="empty">No threads found
|
|
6538
|
+
if (!threads.length) { el.innerHTML = '<div class="empty">No threads found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Threads are created when agents discuss issues. Check the Issues tab to start a discussion.</div></div>'; return; }
|
|
5936
6539
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
5937
6540
|
<thead><tr style="color:var(--text-xs);text-align:left">
|
|
5938
6541
|
<th style="padding:5px 8px">Subject</th><th>Author</th><th>Messages</th><th>Issue</th><th>Created</th>
|
|
5939
6542
|
</tr></thead>
|
|
5940
6543
|
<tbody>${threads.map(t => `<tr style="border-top:1px solid var(--border)">
|
|
5941
|
-
<td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((t.subject||t.title||'—').slice(0,50))}</td>
|
|
6544
|
+
<td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.subject||t.title||'')}">${esc((t.subject||t.title||'—').slice(0,50))}${(t.subject||t.title||'').length > 50 ? '…' : ''}</td>
|
|
5942
6545
|
<td style="padding:5px 8px;color:var(--text-lo)">${esc(t.author||t.createdBy||'—')}</td>
|
|
5943
6546
|
<td style="padding:5px 8px;color:var(--text-lo);text-align:center">${t.messageCount ?? t.messages ?? '—'}</td>
|
|
5944
|
-
<td style="padding:5px 8px;color:var(--text-lo);font-family:var(--mono);font-size:11px">${t.issueId ? '#'+esc(t.issueId.toString().slice(0,8)) : '—'}</td>
|
|
5945
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${t.createdAt ? relTime(t.createdAt) : '—'}</td>
|
|
6547
|
+
<td style="padding:5px 8px;color:var(--text-lo);font-family:var(--mono);font-size:11px" title="${t.issueId ? esc(t.issueId.toString()) : ''}">${t.issueId ? '#'+esc(t.issueId.toString().slice(0,8))+(t.issueId.toString().length > 8 ? '…' : '') : '—'}</td>
|
|
6548
|
+
<td style="padding:5px 8px;color:var(--text-lo)" title="${t.createdAt ? new Date(typeof t.createdAt === 'number' ? t.createdAt : Number(t.createdAt) || t.createdAt).toLocaleString() : ''}">${t.createdAt ? relTime(t.createdAt) : '—'}</td>
|
|
5946
6549
|
</tr>`).join('')}</tbody>
|
|
5947
6550
|
</table>`;
|
|
5948
6551
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
@@ -5952,19 +6555,103 @@ async function v2RenderOrgThreads() {
|
|
|
5952
6555
|
window.v2StopOrg = async function() {
|
|
5953
6556
|
if (!_v2SelOrg) return;
|
|
5954
6557
|
const stopped = _v2SelOrg;
|
|
5955
|
-
try {
|
|
6558
|
+
try {
|
|
6559
|
+
await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'});
|
|
6560
|
+
showToast('Stopped', `Org "${stopped}" stopped`, 'ok');
|
|
6561
|
+
} catch(e) { showToast('Error', e.message, 'err'); }
|
|
5956
6562
|
setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
|
|
5957
6563
|
};
|
|
5958
6564
|
|
|
6565
|
+
// ── Copy org dialog ────────────────────────────────────────────────────────────
|
|
6566
|
+
window.v2ShowCopyOrgDialog = async function() {
|
|
6567
|
+
if (!_v2SelOrg) return;
|
|
6568
|
+
const dialog = document.getElementById('org-copy-dialog');
|
|
6569
|
+
const feedback = document.getElementById('org-copy-feedback');
|
|
6570
|
+
const select = document.getElementById('org-copy-dest-select');
|
|
6571
|
+
const input = document.getElementById('org-copy-dest-input');
|
|
6572
|
+
if (!dialog) return;
|
|
6573
|
+
feedback.textContent = '';
|
|
6574
|
+
input.value = '';
|
|
6575
|
+
select.innerHTML = '<option value="">Loading…</option>';
|
|
6576
|
+
dialog.style.display = 'flex';
|
|
6577
|
+
// Populate project list
|
|
6578
|
+
try {
|
|
6579
|
+
const data = await apiFetch('/api/projects');
|
|
6580
|
+
const projects = (data.projects || []).filter(p => p.path && p.path !== DIR);
|
|
6581
|
+
select.innerHTML = '<option value="">— select a project —</option>' +
|
|
6582
|
+
projects.map(p => `<option value="${esc(p.path)}">${esc(p.name)} (${esc(p.path)})</option>`).join('') +
|
|
6583
|
+
'<option value="__custom__">Custom path…</option>';
|
|
6584
|
+
} catch(e) {
|
|
6585
|
+
select.innerHTML = '<option value="__custom__">Enter path manually</option>';
|
|
6586
|
+
}
|
|
6587
|
+
};
|
|
6588
|
+
|
|
6589
|
+
window.v2HideCopyOrgDialog = function() {
|
|
6590
|
+
const dialog = document.getElementById('org-copy-dialog');
|
|
6591
|
+
if (dialog) dialog.style.display = 'none';
|
|
6592
|
+
};
|
|
6593
|
+
|
|
6594
|
+
window.v2DoCopyOrg = async function() {
|
|
6595
|
+
if (!_v2SelOrg) return;
|
|
6596
|
+
const select = document.getElementById('org-copy-dest-select');
|
|
6597
|
+
const input = document.getElementById('org-copy-dest-input');
|
|
6598
|
+
const feedback = document.getElementById('org-copy-feedback');
|
|
6599
|
+
const btn = document.getElementById('org-copy-confirm-btn');
|
|
6600
|
+
const destination = (input.value.trim()) || (select.value !== '__custom__' ? select.value : '');
|
|
6601
|
+
if (!destination) {
|
|
6602
|
+
feedback.textContent = 'Please select or enter a destination.';
|
|
6603
|
+
feedback.style.color = 'var(--red, #e55)';
|
|
6604
|
+
return;
|
|
6605
|
+
}
|
|
6606
|
+
btn.disabled = true;
|
|
6607
|
+
btn.textContent = 'Copying…';
|
|
6608
|
+
feedback.textContent = '';
|
|
6609
|
+
try {
|
|
6610
|
+
const r = await fetch(`/api/orgs/${encodeURIComponent(_v2SelOrg)}/copy`, {
|
|
6611
|
+
method: 'POST',
|
|
6612
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6613
|
+
body: JSON.stringify({ destination }),
|
|
6614
|
+
});
|
|
6615
|
+
const json = await r.json();
|
|
6616
|
+
if (r.ok && json.ok) {
|
|
6617
|
+
feedback.textContent = '✓ Copied successfully to ' + destination;
|
|
6618
|
+
feedback.style.color = 'var(--green, #4ade80)';
|
|
6619
|
+
setTimeout(v2HideCopyOrgDialog, 1800);
|
|
6620
|
+
} else {
|
|
6621
|
+
feedback.textContent = 'Error: ' + (json.error || r.status);
|
|
6622
|
+
feedback.style.color = 'var(--red, #e55)';
|
|
6623
|
+
}
|
|
6624
|
+
} catch(e) {
|
|
6625
|
+
feedback.textContent = 'Error: ' + e.message;
|
|
6626
|
+
feedback.style.color = 'var(--red, #e55)';
|
|
6627
|
+
} finally {
|
|
6628
|
+
btn.disabled = false;
|
|
6629
|
+
btn.textContent = 'Copy';
|
|
6630
|
+
}
|
|
6631
|
+
};
|
|
6632
|
+
|
|
5959
6633
|
// live SSE for org events
|
|
5960
6634
|
(function v2OrgSSE() {
|
|
5961
6635
|
let src;
|
|
6636
|
+
let _connectTime = 0;
|
|
5962
6637
|
function connect() {
|
|
5963
6638
|
if (src) src.close();
|
|
6639
|
+
_connectTime = Date.now();
|
|
5964
6640
|
src = new EventSource('/api/mastermind-stream');
|
|
5965
6641
|
src.onmessage = e => {
|
|
5966
6642
|
try {
|
|
5967
6643
|
const ev = JSON.parse(e.data);
|
|
6644
|
+
if (ev?.type?.startsWith('loop:')) {
|
|
6645
|
+
// Skip replayed historical events (server replays last 50 on connect)
|
|
6646
|
+
if (ev.ts && ev.ts < _connectTime) return;
|
|
6647
|
+
if (currentView === 'loops') renderLoops();
|
|
6648
|
+
else viewRendered['loops'] = false; // stale — re-fetch on next switchView
|
|
6649
|
+
loadLoopMetrics();
|
|
6650
|
+
if (_mmCurrentTab === 'loops' && document.getElementById('mm-overlay')?.classList.contains('open')) {
|
|
6651
|
+
mmRenderLoops(document.getElementById('mm-body'));
|
|
6652
|
+
}
|
|
6653
|
+
return;
|
|
6654
|
+
}
|
|
5968
6655
|
if (!ev?.org || !ev?.type?.startsWith('org:')) return;
|
|
5969
6656
|
const n = ev.org;
|
|
5970
6657
|
// Filter by project dir if the event carries one; skip events from other projects.
|
|
@@ -6033,11 +6720,12 @@ function buildTimeline(events) {
|
|
|
6033
6720
|
// Only tool + user events with timestamps
|
|
6034
6721
|
const stamped = events.filter(ev => ev.ts && (ev.kind === 'tool' || ev.kind === 'user'));
|
|
6035
6722
|
if (stamped.length < 2) { tl.innerHTML = ''; return; }
|
|
6036
|
-
const
|
|
6723
|
+
const tsMs = ev => typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || 0;
|
|
6724
|
+
const times = stamped.map(tsMs);
|
|
6037
6725
|
const tMin = Math.min(...times), tMax = Math.max(...times);
|
|
6038
6726
|
const span = tMax - tMin || 1;
|
|
6039
6727
|
const segs = stamped.map(ev => {
|
|
6040
|
-
const pct = ((
|
|
6728
|
+
const pct = ((tsMs(ev) - tMin) / span * 100).toFixed(2);
|
|
6041
6729
|
const cat = ev.kind === 'user' ? 'user' : (ev.cat || 'other');
|
|
6042
6730
|
const color = TL_COLORS[cat] || TL_COLORS.other;
|
|
6043
6731
|
const label = ev.kind === 'user' ? 'user message' : (ev.label || ev.name || cat);
|
|
@@ -6070,7 +6758,7 @@ function buildWowDelta() {
|
|
|
6070
6758
|
for (const s of allSessions) {
|
|
6071
6759
|
const ts = s.lastTs || s.mtime;
|
|
6072
6760
|
if (!ts) continue;
|
|
6073
|
-
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
6761
|
+
const age = now - (typeof ts === 'number' ? ts : Number(ts) || new Date(ts).getTime() || 0);
|
|
6074
6762
|
if (age < 7 * DAY) thisWeek++;
|
|
6075
6763
|
else if (age < 14 * DAY) lastWeek++;
|
|
6076
6764
|
}
|
|
@@ -6132,12 +6820,13 @@ async function copySession() {
|
|
|
6132
6820
|
try {
|
|
6133
6821
|
const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
|
|
6134
6822
|
const events = data.events || [];
|
|
6135
|
-
const
|
|
6823
|
+
const _sessTs = sess.lastTs || sess.mtime;
|
|
6824
|
+
const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(typeof _sessTs === 'number' ? _sessTs : Number(_sessTs) || _sessTs).toLocaleString()}`, ''];
|
|
6136
6825
|
for (const ev of events) {
|
|
6137
6826
|
if (ev.kind === 'user' && ev.text?.trim()) {
|
|
6138
6827
|
lines.push('**User:** ' + ev.text.trim().replace(/\n/g, ' '));
|
|
6139
6828
|
} else if (ev.kind === 'tool') {
|
|
6140
|
-
lines.push(`- \`${ev.name || ev.cat}
|
|
6829
|
+
lines.push(`- \`${ev.name || ev.cat}\`${ev.label ? ': ' + ev.label : ''}`);
|
|
6141
6830
|
}
|
|
6142
6831
|
}
|
|
6143
6832
|
await navigator.clipboard.writeText(lines.join('\n'));
|
|
@@ -6204,7 +6893,10 @@ function cmdSearch(q) {
|
|
|
6204
6893
|
results.innerHTML = sq.length >= 2
|
|
6205
6894
|
? '<div class="cmd-empty">Searching sessions…</div>'
|
|
6206
6895
|
: '<div class="cmd-empty">Type at least 2 chars after > to search all sessions</div>';
|
|
6207
|
-
if (sq.length >= 2)
|
|
6896
|
+
if (sq.length >= 2) {
|
|
6897
|
+
clearTimeout(cmdSearch._debounce);
|
|
6898
|
+
cmdSearch._debounce = setTimeout(() => searchSessions(sq), 300);
|
|
6899
|
+
}
|
|
6208
6900
|
return;
|
|
6209
6901
|
}
|
|
6210
6902
|
|
|
@@ -6233,10 +6925,20 @@ function cmdSearch(q) {
|
|
|
6233
6925
|
|
|
6234
6926
|
// ACTIONS group
|
|
6235
6927
|
const actionItems = [
|
|
6236
|
-
{ label: '
|
|
6928
|
+
{ label: 'Go to Now', action: () => switchView('now') },
|
|
6929
|
+
{ label: 'Go to Sessions', action: () => switchView('sessions') },
|
|
6930
|
+
{ label: 'Go to Projects', action: () => switchView('projects') },
|
|
6931
|
+
{ label: 'Go to Loops', action: () => switchView('loops') },
|
|
6932
|
+
{ label: 'Go to Tokens', action: () => switchView('tokens') },
|
|
6933
|
+
{ label: 'Go to Memory', action: () => switchView('memory') },
|
|
6934
|
+
{ label: 'Go to Orgs', action: () => switchView('orgs') },
|
|
6935
|
+
{ label: 'Go to Monograph', action: () => switchView('monograph') },
|
|
6936
|
+
{ label: 'Go to Global Feed',action: () => switchView('global') },
|
|
6237
6937
|
{ label: 'Refresh Dashboard', action: () => refreshCurrent() },
|
|
6238
6938
|
{ label: 'Toggle Compact View', action: () => toggleDensity() },
|
|
6239
|
-
{ label: '
|
|
6939
|
+
{ label: 'Toggle Ambient Mode', action: () => toggleAmbient() },
|
|
6940
|
+
{ label: 'Open Mastermind', action: () => openMastermind() },
|
|
6941
|
+
{ label: 'Keyboard Shortcuts', action: () => { closeCmdPalette(); openShortcutHelp(); } },
|
|
6240
6942
|
].filter(a => !lq || a.label.toLowerCase().includes(lq));
|
|
6241
6943
|
|
|
6242
6944
|
// TABS group — only if org room is open
|
|
@@ -6262,8 +6964,8 @@ function cmdSearch(q) {
|
|
|
6262
6964
|
html += `<div class="cmd-item" data-ci="${idx}">
|
|
6263
6965
|
<span class="ci-ico">◫</span>
|
|
6264
6966
|
<div class="cmd-item-body">
|
|
6265
|
-
<div class="ci-title">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
|
|
6266
|
-
<div class="ci-sub">${relTime(s.lastTs || s.mtime)}</div>
|
|
6967
|
+
<div class="ci-title" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
|
|
6968
|
+
<div class="ci-sub" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(s.lastTs || s.mtime)}">${relTime(s.lastTs || s.mtime)}</div>
|
|
6267
6969
|
</div>
|
|
6268
6970
|
</div>`;
|
|
6269
6971
|
});
|
|
@@ -6278,7 +6980,7 @@ function cmdSearch(q) {
|
|
|
6278
6980
|
<span class="ci-ico">◈</span>
|
|
6279
6981
|
<div class="cmd-item-body">
|
|
6280
6982
|
<div class="ci-title">${esc(d.key || d.namespace || '—')}</div>
|
|
6281
|
-
<div class="ci-sub">${esc(String(d.value || d.text || '').slice(0, 60))}</div>
|
|
6983
|
+
<div class="ci-sub" title="${esc(String(d.value || d.text || ''))}">${esc(String(d.value || d.text || '').slice(0, 60))}${String(d.value || d.text || '').length > 60 ? '…' : ''}</div>
|
|
6282
6984
|
</div>
|
|
6283
6985
|
</div>`;
|
|
6284
6986
|
});
|
|
@@ -6324,7 +7026,7 @@ function cmdSearch(q) {
|
|
|
6324
7026
|
<span class="ci-ico">✦</span>
|
|
6325
7027
|
<div class="cmd-item-body">
|
|
6326
7028
|
<div class="ci-title">${esc(skLabel)}</div>
|
|
6327
|
-
<div class="ci-sub">${typeof sk === 'string' ? '' : esc((sk.description || '').slice(0, 60))}</div>
|
|
7029
|
+
<div class="ci-sub" title="${typeof sk === 'string' ? '' : esc(sk.description || '')}">${typeof sk === 'string' ? '' : esc((sk.description || '').slice(0, 60)) + ((sk.description || '').length > 60 ? '…' : '')}</div>
|
|
6328
7030
|
</div>
|
|
6329
7031
|
</div>`;
|
|
6330
7032
|
});
|
|
@@ -6391,7 +7093,7 @@ function executeCmdItem() {
|
|
|
6391
7093
|
else if (item.type === 'memory') switchView('memory');
|
|
6392
7094
|
else if (item.type === 'project') switchProject(item.data.path);
|
|
6393
7095
|
else if (item.type === 'org') { switchView('orgs'); setTimeout(() => v2SelectOrg(item.data.name), 80); }
|
|
6394
|
-
else if (item.type === 'skill')
|
|
7096
|
+
else if (item.type === 'skill') { const _skN = typeof item.data === 'string' ? item.data : (item.data.name || item.data.id || ''); _mmSkillFilter = _skN; openMastermind(); mmSwitchTab('skills'); }
|
|
6395
7097
|
else if (item.type === 'action') { if (item.data.action) item.data.action(); }
|
|
6396
7098
|
else if (item.type === 'orgtab') { const btn = document.querySelector(`.odt-btn[data-tab="${item.data.tab}"]`); if (btn) btn.click(); }
|
|
6397
7099
|
}
|
|
@@ -6414,8 +7116,8 @@ async function searchSessions(q) {
|
|
|
6414
7116
|
html += `<div class="cmd-item" data-ci="${idx}">
|
|
6415
7117
|
<span class="ci-ico">◫</span>
|
|
6416
7118
|
<div class="cmd-item-body">
|
|
6417
|
-
<div class="ci-title">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
|
|
6418
|
-
<div class="ci-sub">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
|
|
7119
|
+
<div class="ci-title" title="${esc(r.lastPrompt || r.id || '')}">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
|
|
7120
|
+
<div class="ci-sub" title="${esc(snippet)}">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
|
|
6419
7121
|
</div>
|
|
6420
7122
|
</div>`;
|
|
6421
7123
|
});
|
|
@@ -6440,18 +7142,22 @@ document.addEventListener('keydown', e => {
|
|
|
6440
7142
|
return;
|
|
6441
7143
|
}
|
|
6442
7144
|
|
|
7145
|
+
// Escape always closes modals, even when focused inside an input/textarea
|
|
7146
|
+
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); closeBudgetModal(); closeChunkModal(); closeMemModal(); closeReportCard(); closeMastermind(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); return; }
|
|
7147
|
+
|
|
6443
7148
|
// ignore when typing in inputs
|
|
6444
7149
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
6445
7150
|
if (document.getElementById('cmd-palette').classList.contains('open')) return;
|
|
6446
|
-
|
|
6447
|
-
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
|
|
6448
7151
|
if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
|
|
7152
|
+
if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); return; }
|
|
7153
|
+
if (e.key === 'm' || e.key === 'M') { e.preventDefault(); openMastermind(); return; }
|
|
6449
7154
|
if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
|
|
7155
|
+
const _viewKeys = {'1':'now','2':'sessions','3':'projects','4':'loops','5':'tokens','6':'memory','7':'orgs','8':'monograph','9':'global'};
|
|
7156
|
+
if (_viewKeys[e.key] && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); switchView(_viewKeys[e.key]); return; }
|
|
6450
7157
|
|
|
6451
7158
|
if (currentView === 'now') {
|
|
6452
7159
|
if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
|
|
6453
7160
|
if (e.key === 'g' || e.key === 'G') { e.preventDefault(); goLive(); }
|
|
6454
|
-
if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); }
|
|
6455
7161
|
|
|
6456
7162
|
if (e.key === 'j' || e.key === 'k') {
|
|
6457
7163
|
e.preventDefault();
|
|
@@ -6519,6 +7225,22 @@ function filterSessions(q) {
|
|
|
6519
7225
|
});
|
|
6520
7226
|
const countEl = document.getElementById('sess-filter-count');
|
|
6521
7227
|
if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
|
|
7228
|
+
// zero-results empty state
|
|
7229
|
+
const noRes = document.getElementById('sess-filter-noresult');
|
|
7230
|
+
if (lq && visible === 0 && rows.length > 0) {
|
|
7231
|
+
if (!noRes) {
|
|
7232
|
+
const el = document.createElement('div');
|
|
7233
|
+
el.id = 'sess-filter-noresult';
|
|
7234
|
+
el.className = 'empty';
|
|
7235
|
+
el.style.cssText = 'padding:24px 0;font-size:13px';
|
|
7236
|
+
el.textContent = 'No sessions match "' + q.slice(0, 40) + '"';
|
|
7237
|
+
document.getElementById('sess-content').appendChild(el);
|
|
7238
|
+
} else {
|
|
7239
|
+
noRes.textContent = 'No sessions match "' + q.slice(0, 40) + '"';
|
|
7240
|
+
}
|
|
7241
|
+
} else if (noRes) {
|
|
7242
|
+
noRes.remove();
|
|
7243
|
+
}
|
|
6522
7244
|
}
|
|
6523
7245
|
|
|
6524
7246
|
// ── feature 32: keyboard shortcut help modal ──────────────
|
|
@@ -6545,7 +7267,7 @@ function updateCurrentActivity(events) {
|
|
|
6545
7267
|
} else if (name) {
|
|
6546
7268
|
activity = name;
|
|
6547
7269
|
}
|
|
6548
|
-
if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
|
|
7270
|
+
if (activity) { el.textContent = '⤷ ' + activity; el.title = recent.label || name || ''; el.classList.add('loaded'); }
|
|
6549
7271
|
else { el.textContent = ''; el.classList.remove('loaded'); }
|
|
6550
7272
|
}
|
|
6551
7273
|
|
|
@@ -6569,7 +7291,8 @@ function buildPatterns() {
|
|
|
6569
7291
|
if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
|
|
6570
7292
|
}
|
|
6571
7293
|
}
|
|
6572
|
-
const
|
|
7294
|
+
const allTerms = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]);
|
|
7295
|
+
const sorted = allTerms.slice(0, 20);
|
|
6573
7296
|
if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
|
|
6574
7297
|
const maxCount = sorted[0][1];
|
|
6575
7298
|
const rows = sorted.map(([word, count], i) => {
|
|
@@ -6582,7 +7305,8 @@ function buildPatterns() {
|
|
|
6582
7305
|
}).join('');
|
|
6583
7306
|
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
6584
7307
|
<th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
|
|
6585
|
-
</tr></thead><tbody>${rows}</tbody></table
|
|
7308
|
+
</tr></thead><tbody>${rows}</tbody></table>` +
|
|
7309
|
+
(allTerms.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${allTerms.length} terms</div>` : '');
|
|
6586
7310
|
}
|
|
6587
7311
|
|
|
6588
7312
|
// ── feature 35: session streak tracker ────────────────────
|
|
@@ -6590,7 +7314,7 @@ function calcStreak() {
|
|
|
6590
7314
|
const dates = new Set(allSessions.map(s => {
|
|
6591
7315
|
const t = s.firstTs || s.mtime;
|
|
6592
7316
|
if (!t) return null;
|
|
6593
|
-
return new Date(typeof t === 'number' ? t : t).toDateString();
|
|
7317
|
+
return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString();
|
|
6594
7318
|
}).filter(Boolean));
|
|
6595
7319
|
let streak = 0;
|
|
6596
7320
|
const today = new Date();
|
|
@@ -6608,9 +7332,16 @@ function calcStreak() {
|
|
|
6608
7332
|
|
|
6609
7333
|
// ── feature 25: notification toasts ──────────────────────
|
|
6610
7334
|
let _toastLastBudgetKey = '';
|
|
7335
|
+
let _toastLastKey = ''; let _toastLastTs = 0;
|
|
6611
7336
|
function showToast(title, msg, type = 'info', duration = 5000) {
|
|
6612
7337
|
const rack = document.getElementById('toast-rack');
|
|
6613
7338
|
if (!rack) return;
|
|
7339
|
+
// Dedup: skip identical toast within 2s
|
|
7340
|
+
const key = type + '|' + title + '|' + msg;
|
|
7341
|
+
if (key === _toastLastKey && Date.now() - _toastLastTs < 2000) return;
|
|
7342
|
+
_toastLastKey = key; _toastLastTs = Date.now();
|
|
7343
|
+
// Cap at 5 visible toasts — evict oldest
|
|
7344
|
+
while (rack.children.length >= 5) rack.firstChild.remove();
|
|
6614
7345
|
const icoMap = { warn: '⚑', err: '⚠', ok: '✓', info: '◉' };
|
|
6615
7346
|
const div = document.createElement('div');
|
|
6616
7347
|
div.className = 'toast t-' + type;
|
|
@@ -6619,13 +7350,13 @@ function showToast(title, msg, type = 'info', duration = 5000) {
|
|
|
6619
7350
|
<div class="toast-title">${esc(title)}</div>
|
|
6620
7351
|
<div class="toast-msg">${esc(msg)}</div>
|
|
6621
7352
|
</div>
|
|
6622
|
-
<button class="toast-close" onclick="this.closest('.toast').remove()">✕</button>`;
|
|
7353
|
+
<button class="toast-close" onclick="this.closest('.toast').remove()" title="Dismiss">✕</button>`;
|
|
6623
7354
|
rack.appendChild(div);
|
|
6624
7355
|
if (duration > 0) setTimeout(() => { try { div.remove(); } catch {} }, duration);
|
|
6625
7356
|
}
|
|
6626
7357
|
|
|
6627
7358
|
function checkBudgetToast(todayCost, monthCost) {
|
|
6628
|
-
const budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
|
|
7359
|
+
const budget = (function(){ try { return JSON.parse(localStorage.getItem('mm-budget') || '{}'); } catch { return {}; } })();
|
|
6629
7360
|
const daily = parseFloat(budget.daily) || 0;
|
|
6630
7361
|
const monthly = parseFloat(budget.monthly) || 0;
|
|
6631
7362
|
if (daily > 0 && todayCost >= daily * 0.9) {
|
|
@@ -6665,7 +7396,7 @@ function periodFilteredSessions() {
|
|
|
6665
7396
|
if (w === Infinity) return allSessions;
|
|
6666
7397
|
return allSessions.filter(s => {
|
|
6667
7398
|
const t = s.firstTs || s.mtime; if (!t) return false;
|
|
6668
|
-
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
7399
|
+
const ts = typeof t === 'number' ? t : Number(t) || new Date(t).getTime() || 0;
|
|
6669
7400
|
return (now - ts) <= w;
|
|
6670
7401
|
});
|
|
6671
7402
|
}
|
|
@@ -6685,12 +7416,16 @@ function buildSessionHeatmap(sessions) {
|
|
|
6685
7416
|
}
|
|
6686
7417
|
for (const s of sessions) {
|
|
6687
7418
|
const ts = s.lastTs || s.mtime; if (!ts) continue;
|
|
6688
|
-
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
7419
|
+
const age = now - (typeof ts === 'number' ? ts : Number(ts) || new Date(ts).getTime() || 0);
|
|
6689
7420
|
const idx = DAYS - 1 - Math.floor(age / DAY);
|
|
6690
7421
|
if (idx >= 0 && idx < DAYS) buckets[idx].count++;
|
|
6691
7422
|
}
|
|
6692
7423
|
const max = Math.max(...buckets.map(b => b.count), 1);
|
|
6693
|
-
|
|
7424
|
+
// pad so column 0 starts on Monday
|
|
7425
|
+
const firstDowShm = new Date(now - (DAYS - 1) * DAY).getDay(); // 0=Sun
|
|
7426
|
+
const shmOffset = firstDowShm === 0 ? 6 : firstDowShm - 1;
|
|
7427
|
+
const padShm = Array.from({ length: shmOffset }, () => '<div class="shm-cell" style="opacity:0;pointer-events:none"></div>');
|
|
7428
|
+
el.innerHTML = padShm.join('') + buckets.map(b => {
|
|
6694
7429
|
const level = b.count === 0 ? 0 : Math.min(4, Math.ceil(b.count / max * 4));
|
|
6695
7430
|
const isActive = b.date === heatmapDateFilter;
|
|
6696
7431
|
return `<div class="shm-cell shm-${level}${isActive ? ' shm-active' : ''}" title="${b.date}: ${b.count} session${b.count !== 1 ? 's' : ''}" onclick="setHeatmapFilter('${b.date}',${b.count})"></div>`;
|
|
@@ -6784,7 +7519,8 @@ function bulkExport() {
|
|
|
6784
7519
|
if (!toExport.length) return;
|
|
6785
7520
|
const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'Files Touched'];
|
|
6786
7521
|
const rows = toExport.map(s => {
|
|
6787
|
-
const
|
|
7522
|
+
const _ts0 = s.firstTs || s.mtime || 0;
|
|
7523
|
+
const dt = new Date(typeof _ts0 === 'number' ? _ts0 : Number(_ts0) || _ts0).toISOString().slice(0, 19).replace('T', ' ');
|
|
6788
7524
|
const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
|
|
6789
7525
|
const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
|
|
6790
7526
|
const prompt = (s.lastPrompt || '').replace(/"/g, '""');
|
|
@@ -6811,7 +7547,7 @@ function buildTokenVelocity() {
|
|
|
6811
7547
|
for (const s of filtered) {
|
|
6812
7548
|
const t = s.firstTs || s.mtime;
|
|
6813
7549
|
if (!t) continue;
|
|
6814
|
-
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
7550
|
+
const ts = typeof t === 'number' ? t : Number(t) || new Date(t).getTime() || 0;
|
|
6815
7551
|
const hoursAgo = (now - ts) / HOUR;
|
|
6816
7552
|
if (hoursAgo < 0 || hoursAgo >= 24) continue;
|
|
6817
7553
|
const bucket = Math.min(23, Math.floor(23 - hoursAgo));
|
|
@@ -6838,7 +7574,8 @@ function exportSessionsCSV() {
|
|
|
6838
7574
|
if (!allSessions.length) { showToast('No data', 'No sessions loaded yet', 'warn'); return; }
|
|
6839
7575
|
const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'User Messages', 'Cache Hit %', 'Input Tokens'];
|
|
6840
7576
|
const rows = allSessions.map(s => {
|
|
6841
|
-
const
|
|
7577
|
+
const _ts1 = s.firstTs || s.mtime || 0;
|
|
7578
|
+
const dt = new Date(typeof _ts1 === 'number' ? _ts1 : Number(_ts1) || _ts1).toISOString().slice(0, 19).replace('T', ' ');
|
|
6842
7579
|
const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
|
|
6843
7580
|
const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
|
|
6844
7581
|
const cachePct = s.totalInputTokens > 0 ? Math.round((s.cacheReadTokens || 0) / s.totalInputTokens * 100) : '';
|
|
@@ -6871,11 +7608,12 @@ async function loadToolRank() {
|
|
|
6871
7608
|
const data = await apiFetch('/api/tool-ranking?dir=' + enc(DIR));
|
|
6872
7609
|
if (!data.tools?.length) { el.innerHTML = '<div class="loading-txt">No tool usage data</div>'; return; }
|
|
6873
7610
|
const maxCount = data.tools[0].count;
|
|
6874
|
-
const
|
|
7611
|
+
const shownTools = data.tools.slice(0, 15);
|
|
7612
|
+
const rows = shownTools.map((t, i) => {
|
|
6875
7613
|
const barW = Math.round((t.count / maxCount) * 100);
|
|
6876
7614
|
const errRate = t.errors > 0 ? ((t.errors / t.count) * 100).toFixed(0) + '%' : '—';
|
|
6877
7615
|
return `<tr><td class="lb-rank">${i + 1}</td>
|
|
6878
|
-
<td style="font-size:12px;color:var(--text-mid);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.tool)}</td>
|
|
7616
|
+
<td style="font-size:12px;color:var(--text-mid);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.tool)}">${esc(t.tool)}</td>
|
|
6879
7617
|
<td style="width:80px;padding:4px 6px">
|
|
6880
7618
|
<div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
|
|
6881
7619
|
<div style="height:100%;width:${barW}%;background:oklch(65% 0.15 200);border-radius:2px"></div>
|
|
@@ -6887,7 +7625,8 @@ async function loadToolRank() {
|
|
|
6887
7625
|
}).join('');
|
|
6888
7626
|
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
6889
7627
|
<th class="lb-rank">#</th><th>Tool</th><th></th><th class="lb-cost">Calls</th><th class="lb-dur">Error%</th>
|
|
6890
|
-
</tr></thead><tbody>${rows}</tbody></table
|
|
7628
|
+
</tr></thead><tbody>${rows}</tbody></table>` +
|
|
7629
|
+
(data.tools.length > 15 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 15 of ${data.tools.length} tools</div>` : '');
|
|
6891
7630
|
} catch (err) {
|
|
6892
7631
|
el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
|
|
6893
7632
|
}
|
|
@@ -6909,13 +7648,14 @@ async function loadProjCosts() {
|
|
|
6909
7648
|
const data = await apiFetch('/api/project-costs');
|
|
6910
7649
|
if (!data.projects?.length) { el.innerHTML = '<div class="loading-txt">No cost data across projects</div>'; return; }
|
|
6911
7650
|
const maxCost = data.projects[0].cost;
|
|
6912
|
-
const
|
|
7651
|
+
const shownProjects = data.projects.slice(0, 10);
|
|
7652
|
+
const rows = shownProjects.map((p, i) => {
|
|
6913
7653
|
const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
|
|
6914
7654
|
const name = p.path.split('/').filter(Boolean).pop() || p.path;
|
|
6915
7655
|
return `<tr onclick="switchProject('${esc(p.path)}')" style="cursor:pointer" title="${esc(p.path)}">
|
|
6916
7656
|
<td class="lb-rank">${i + 1}</td>
|
|
6917
7657
|
<td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
|
|
6918
|
-
<td class="lb-cost">$${p.cost.toFixed(2)}</td>
|
|
7658
|
+
<td class="lb-cost">$${(p.cost || 0).toFixed(2)}</td>
|
|
6919
7659
|
<td style="width:80px;padding:4px 6px">
|
|
6920
7660
|
<div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
|
|
6921
7661
|
<div style="height:100%;width:${barW}%;background:oklch(72% 0.18 75 / 0.7);border-radius:2px"></div>
|
|
@@ -6926,7 +7666,8 @@ async function loadProjCosts() {
|
|
|
6926
7666
|
}).join('');
|
|
6927
7667
|
el.innerHTML = `<table class="lb-table"><thead><tr>
|
|
6928
7668
|
<th class="lb-rank">#</th><th>Project</th><th class="lb-cost">Cost</th><th></th><th class="lb-dur">Sessions</th>
|
|
6929
|
-
</tr></thead><tbody>${rows}</tbody></table
|
|
7669
|
+
</tr></thead><tbody>${rows}</tbody></table>` +
|
|
7670
|
+
(data.projects.length > 10 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 10 of ${data.projects.length} projects</div>` : '');
|
|
6930
7671
|
} catch (err) {
|
|
6931
7672
|
el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
|
|
6932
7673
|
}
|
|
@@ -6954,7 +7695,7 @@ function buildGanttTimeline() {
|
|
|
6954
7695
|
}
|
|
6955
7696
|
for (const s of allSessions) {
|
|
6956
7697
|
const t = s.firstTs || s.mtime; if (!t) continue;
|
|
6957
|
-
const d = new Date(typeof t === 'number' ? t : t); d.setHours(0,0,0,0);
|
|
7698
|
+
const d = new Date(typeof t === 'number' ? t : Number(t) || t); d.setHours(0,0,0,0);
|
|
6958
7699
|
const key = d.toDateString();
|
|
6959
7700
|
if (key in days) days[key].push(s);
|
|
6960
7701
|
}
|
|
@@ -6972,11 +7713,11 @@ function buildGanttTimeline() {
|
|
|
6972
7713
|
// Sort sessions by start time
|
|
6973
7714
|
const sorted = [...sessions].sort((a, b) => {
|
|
6974
7715
|
const ta = a.firstTs || a.mtime || 0; const tb = b.firstTs || b.mtime || 0;
|
|
6975
|
-
return (typeof ta === 'number' ? ta : new Date(ta).getTime()) - (typeof tb === 'number' ? tb : new Date(tb).getTime());
|
|
7716
|
+
return (typeof ta === 'number' ? ta : Number(ta) || new Date(ta).getTime() || 0) - (typeof tb === 'number' ? tb : Number(tb) || new Date(tb).getTime() || 0);
|
|
6976
7717
|
});
|
|
6977
7718
|
for (let i = 0; i < sorted.length; i++) {
|
|
6978
7719
|
const s = sorted[i];
|
|
6979
|
-
const
|
|
7720
|
+
const _sT = s.firstTs || s.mtime; const startTs = typeof _sT === 'number' ? _sT : Number(_sT) || new Date(_sT).getTime() || 0;
|
|
6980
7721
|
const dayStart = new Date(dateStr).getTime();
|
|
6981
7722
|
const startPct = Math.min(95, ((startTs - dayStart) / DAY) * 100);
|
|
6982
7723
|
const durPct = s.totalDurationMs ? Math.max(0.5, Math.min(15, (s.totalDurationMs / DAY) * 100)) : 1;
|
|
@@ -7003,11 +7744,11 @@ function showReportCard() {
|
|
|
7003
7744
|
|
|
7004
7745
|
const todaySess = allSessions.filter(s => {
|
|
7005
7746
|
const t = s.firstTs || s.mtime; if (!t) return false;
|
|
7006
|
-
return new Date(typeof t === 'number' ? t : t).toDateString() === todayStr;
|
|
7747
|
+
return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString() === todayStr;
|
|
7007
7748
|
});
|
|
7008
7749
|
const weekSess = allSessions.filter(s => {
|
|
7009
7750
|
const t = s.firstTs || s.mtime; if (!t) return false;
|
|
7010
|
-
return (typeof t === 'number' ? t : new Date(t).getTime()) >= weekAgo;
|
|
7751
|
+
return (typeof t === 'number' ? t : Number(t) || new Date(t).getTime() || 0) >= weekAgo;
|
|
7011
7752
|
});
|
|
7012
7753
|
|
|
7013
7754
|
function summarize(sessions, label) {
|
|
@@ -7153,7 +7894,7 @@ function showCostExplainer(sessId, event) {
|
|
|
7153
7894
|
.map(([m,d])=>`<div class="err-item">${esc(m.replace(/^claude-/,'').replace(/-\d{8}$/,''))}: $${(d.cost||0).toFixed(4)} · ${d.calls||0} calls</div>`).join('');
|
|
7154
7895
|
drawer.innerHTML = `<div class="err-drawer-head" style="color:oklch(70% 0.18 80)">
|
|
7155
7896
|
<span>Cost anomaly — $${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
|
|
7156
|
-
<button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''">✕</button>
|
|
7897
|
+
<button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''" title="Close">✕</button>
|
|
7157
7898
|
</div>
|
|
7158
7899
|
<div class="err-drawer-body">
|
|
7159
7900
|
<div class="err-item" style="color:var(--text-lo)">Tool calls: ${sess.toolCalls||0} · Messages: ${sess.totalMessages||0} · Tokens in: ${(sess.totalInputTokens||0).toLocaleString()}</div>
|
|
@@ -7170,7 +7911,7 @@ function buildDailyCostTrend() {
|
|
|
7170
7911
|
const dates = new Array(30).fill(null).map((_, i) => new Date(now - (29 - i) * DAY).toDateString());
|
|
7171
7912
|
for (const s of allSessions) {
|
|
7172
7913
|
const t = s.firstTs || s.mtime; if (!t) continue;
|
|
7173
|
-
const ts = typeof t === 'number' ? t : new Date(t).getTime();
|
|
7914
|
+
const ts = typeof t === 'number' ? t : Number(t) || new Date(t).getTime() || 0;
|
|
7174
7915
|
const daysAgo = Math.floor((now - ts) / DAY);
|
|
7175
7916
|
const idx = 29 - daysAgo;
|
|
7176
7917
|
if (idx >= 0 && idx < 30) buckets[idx] += s.totalCost || 0;
|
|
@@ -7222,7 +7963,7 @@ function buildHourlyHeatmap() {
|
|
|
7222
7963
|
const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
7223
7964
|
for (const s of allSessions) {
|
|
7224
7965
|
const t = s.firstTs || s.mtime; if (!t) continue;
|
|
7225
|
-
const d = new Date(typeof t === 'number' ? t : t);
|
|
7966
|
+
const d = new Date(typeof t === 'number' ? t : Number(t) || t);
|
|
7226
7967
|
const dow = d.getDay(); // 0=Sun
|
|
7227
7968
|
const hour = d.getHours();
|
|
7228
7969
|
grid[dow][hour]++;
|
|
@@ -7253,7 +7994,7 @@ function buildHourlyHeatmap() {
|
|
|
7253
7994
|
|
|
7254
7995
|
// ── feature 50: custom tag editor ─────────────────────────
|
|
7255
7996
|
const _customTagsKey = 'mm-custom-tags';
|
|
7256
|
-
let _customTagsMap = new Map(Object.entries(JSON.parse(localStorage.getItem(_customTagsKey) || '{}')));
|
|
7997
|
+
let _customTagsMap = new Map(Object.entries((function(){ try { return JSON.parse(localStorage.getItem(_customTagsKey) || '{}'); } catch { return {}; } })()));
|
|
7257
7998
|
|
|
7258
7999
|
function getCustomTags(sessId) {
|
|
7259
8000
|
return _customTagsMap.get(sessId) || [];
|
|
@@ -7273,10 +8014,12 @@ function addCustomTag(sessId, tag, event) {
|
|
|
7273
8014
|
if (!tags.includes(t)) { tags.push(t); saveCustomTags(sessId, tags); }
|
|
7274
8015
|
const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
|
|
7275
8016
|
if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
|
|
7276
|
-
// rebuild tag filter bar
|
|
8017
|
+
// rebuild tag filter bar — full re-render if bar was absent (e.g. just transitioned from 0 tags)
|
|
7277
8018
|
initTags();
|
|
7278
8019
|
const tfBar = document.querySelector('.tag-filter-bar');
|
|
7279
|
-
|
|
8020
|
+
const newBarHtml = buildTagFilterBar(allSessions);
|
|
8021
|
+
if (tfBar) tfBar.outerHTML = newBarHtml;
|
|
8022
|
+
else if (newBarHtml) renderSessions();
|
|
7280
8023
|
}
|
|
7281
8024
|
|
|
7282
8025
|
function removeCustomTag(sessId, tag, event) {
|
|
@@ -7285,6 +8028,9 @@ function removeCustomTag(sessId, tag, event) {
|
|
|
7285
8028
|
saveCustomTags(sessId, tags);
|
|
7286
8029
|
const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
|
|
7287
8030
|
if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
|
|
8031
|
+
initTags();
|
|
8032
|
+
const tfBar = document.querySelector('.tag-filter-bar');
|
|
8033
|
+
if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
|
|
7288
8034
|
}
|
|
7289
8035
|
|
|
7290
8036
|
function showCustomTagInput(sessId, event) {
|
|
@@ -7297,7 +8043,7 @@ function showCustomTagInput(sessId, event) {
|
|
|
7297
8043
|
iw.className = 'ctag-input-wrap';
|
|
7298
8044
|
iw.onclick = e => e.stopPropagation();
|
|
7299
8045
|
iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
|
|
7300
|
-
<button class="ctag-ok" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
|
|
8046
|
+
<button class="ctag-ok" title="Add this tag to the session" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
|
|
7301
8047
|
wrap.appendChild(iw);
|
|
7302
8048
|
const inp = iw.querySelector('input');
|
|
7303
8049
|
inp.focus();
|
|
@@ -7328,14 +8074,14 @@ async function toggleErrDrawer(sessId, event) {
|
|
|
7328
8074
|
try {
|
|
7329
8075
|
const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
|
|
7330
8076
|
if (!data.errors?.length) {
|
|
7331
|
-
drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
|
|
8077
|
+
drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')" title="Close">✕</button></div>`;
|
|
7332
8078
|
return;
|
|
7333
8079
|
}
|
|
7334
8080
|
const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
|
|
7335
|
-
drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>
|
|
8081
|
+
drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')" title="Close">✕</button></div>
|
|
7336
8082
|
<div class="err-drawer-body">${items}</div>`;
|
|
7337
8083
|
} catch (err) {
|
|
7338
|
-
drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
|
|
8084
|
+
drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')" title="Close">✕</button></div>`;
|
|
7339
8085
|
}
|
|
7340
8086
|
}
|
|
7341
8087
|
|
|
@@ -7369,7 +8115,7 @@ function buildCostHistogram() {
|
|
|
7369
8115
|
const counts = new Array(BUCKETS).fill(0);
|
|
7370
8116
|
for (const c of costs) { const i = Math.min(BUCKETS - 1, Math.floor((c - minC) / bucketSize)); counts[i]++; }
|
|
7371
8117
|
const maxCount = Math.max(1, ...counts);
|
|
7372
|
-
const fmt = v => v < 0.01 ? v.toFixed(4) : v < 1 ? '$' + v.toFixed(2) : '$' + v.toFixed(1);
|
|
8118
|
+
const fmt = v => v < 0.01 ? '$' + v.toFixed(4) : v < 1 ? '$' + v.toFixed(2) : '$' + v.toFixed(1);
|
|
7373
8119
|
const bars = counts.map((n, i) => {
|
|
7374
8120
|
const h = Math.max(2, Math.round((n / maxCount) * 46));
|
|
7375
8121
|
const lo = minC + i * bucketSize; const hi = lo + bucketSize;
|
|
@@ -7435,18 +8181,25 @@ function esc(s) {
|
|
|
7435
8181
|
|
|
7436
8182
|
function relTime(ts) {
|
|
7437
8183
|
if (!ts) return '';
|
|
7438
|
-
const
|
|
8184
|
+
const abs = typeof ts === 'number' ? ts : (Number(ts) || new Date(ts).getTime() || 0);
|
|
8185
|
+
const diff = Date.now() - abs;
|
|
7439
8186
|
const s = Math.floor(diff / 1000);
|
|
7440
8187
|
if (s < 5) return 'now';
|
|
7441
|
-
if (s < 60) return s + 's';
|
|
8188
|
+
if (s < 60) return s + 's ago';
|
|
7442
8189
|
const m = Math.floor(s / 60);
|
|
7443
|
-
if (m < 60) return m + 'm';
|
|
8190
|
+
if (m < 60) return m + 'm ago';
|
|
7444
8191
|
const h = Math.floor(m / 60);
|
|
7445
|
-
if (h < 24) return h + 'h';
|
|
7446
|
-
|
|
8192
|
+
if (h < 24) return h + 'h ago';
|
|
8193
|
+
const d = Math.floor(h / 24);
|
|
8194
|
+
if (d < 7) return d + 'd ago';
|
|
8195
|
+
const dt = new Date(abs);
|
|
8196
|
+
const mon = dt.toLocaleString('default', { month: 'short' });
|
|
8197
|
+
const sameYear = dt.getFullYear() === new Date().getFullYear();
|
|
8198
|
+
return sameYear ? mon + ' ' + dt.getDate() : mon + ' ' + dt.getDate() + ', ' + dt.getFullYear();
|
|
7447
8199
|
}
|
|
7448
8200
|
|
|
7449
8201
|
function fmtDur(ms) {
|
|
8202
|
+
if (!ms || ms < 1000) return '<1s';
|
|
7450
8203
|
const s = Math.floor(ms / 1000);
|
|
7451
8204
|
if (s < 60) return s + 's';
|
|
7452
8205
|
const m = Math.floor(s / 60);
|
|
@@ -7643,7 +8396,7 @@ function renderMgOverview() {
|
|
|
7643
8396
|
topEl.innerHTML = prSorted.map(n => `
|
|
7644
8397
|
<div class="mg-bar-row">
|
|
7645
8398
|
<div class="mg-bar-lbl" title="${esc(n.label)}">${esc(mgShortLabel(n.label))}</div>
|
|
7646
|
-
<div class="mg-bar-track"><div class="mg-bar-fill" style="width:${maxPR ? Math.round((n.score/maxPR)*100) : 0}%"></div></div>
|
|
8399
|
+
<div class="mg-bar-track" title="${esc(n.label)}: PageRank ${n.score.toExponential(3)}"><div class="mg-bar-fill" style="width:${maxPR ? Math.round((n.score/maxPR)*100) : 0}%"></div></div>
|
|
7647
8400
|
<div class="mg-bar-val">${n.score.toExponential(1)}</div>
|
|
7648
8401
|
</div>`).join('');
|
|
7649
8402
|
}
|
|
@@ -7668,7 +8421,7 @@ function renderMgOverview() {
|
|
|
7668
8421
|
typeEl.innerHTML = typeEntries.map(([t, c]) => `
|
|
7669
8422
|
<div class="mg-bar-row">
|
|
7670
8423
|
<div class="mg-bar-lbl">${esc(t)}</div>
|
|
7671
|
-
<div class="mg-bar-track"><div class="mg-bar-fill mg" style="width:${Math.round((c/maxTC)*100)}%"></div></div>
|
|
8424
|
+
<div class="mg-bar-track" title="${esc(t)}: ${c} node${c!==1?'s':''} (${Math.round((c/maxTC)*100)}%)"><div class="mg-bar-fill mg" style="width:${Math.round((c/maxTC)*100)}%"></div></div>
|
|
7672
8425
|
<div class="mg-bar-val">${c}</div>
|
|
7673
8426
|
</div>`).join('');
|
|
7674
8427
|
}
|
|
@@ -7720,9 +8473,11 @@ async function mgRebuild() {
|
|
|
7720
8473
|
try {
|
|
7721
8474
|
const res = await fetch('/api/monograph-build?dir=' + enc(DIR), { method: 'POST' });
|
|
7722
8475
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
8476
|
+
showToast('Rebuilding', 'Knowledge graph rebuild started', 'info');
|
|
7723
8477
|
_mgLoaded = false;
|
|
7724
8478
|
await loadMonograph();
|
|
7725
|
-
|
|
8479
|
+
showToast('Done', 'Knowledge graph rebuilt', 'ok');
|
|
8480
|
+
} catch (e) { showToast('Error', e.message, 'err'); }
|
|
7726
8481
|
btn.disabled = false; btn.textContent = 'REBUILD';
|
|
7727
8482
|
}
|
|
7728
8483
|
|
|
@@ -7849,14 +8604,16 @@ function mgRunClient(id) {
|
|
|
7849
8604
|
} else if (id === 'pagerank') {
|
|
7850
8605
|
// Proper power-iteration PageRank (d=0.85, 50 iterations)
|
|
7851
8606
|
const prMap = mgComputePageRank(g);
|
|
7852
|
-
const
|
|
8607
|
+
const allRanked = nodes.map(n => {
|
|
7853
8608
|
const k = n.id || n.name || n.label || '';
|
|
7854
8609
|
return { label: n.label || n.name || k, score: prMap[k] || 0 };
|
|
7855
|
-
}).sort((a, b) => b.score - a.score)
|
|
8610
|
+
}).sort((a, b) => b.score - a.score);
|
|
8611
|
+
const ranked = allRanked.slice(0, 20);
|
|
7856
8612
|
const maxScore = ranked.length ? ranked[0].score : 1;
|
|
7857
8613
|
html = `<table class="mg-table"><thead><tr><th>#</th><th>Node</th><th>PageRank</th><th style="width:180px">Weight</th></tr></thead><tbody>` +
|
|
7858
8614
|
ranked.map((r, i) => `<tr><td>${i+1}</td><td title="${esc(r.label)}">${esc(mgShortLabel(r.label))}</td><td>${r.score.toExponential(3)}</td><td><div class="mg-bar-track" style="height:8px"><div class="mg-bar-fill" style="width:${maxScore ? Math.round((r.score/maxScore)*100) : 0}%"></div></div></td></tr>`).join('') +
|
|
7859
|
-
`</tbody></table
|
|
8615
|
+
`</tbody></table>` +
|
|
8616
|
+
(allRanked.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${allRanked.length} nodes</div>` : '');
|
|
7860
8617
|
|
|
7861
8618
|
} else if (id === 'deadcode') {
|
|
7862
8619
|
const isolated = nodes.filter(n => (degMap[n.id || n.name || n.label || ''] || 0) === 0);
|
|
@@ -7888,7 +8645,8 @@ function mgRunClient(id) {
|
|
|
7888
8645
|
<div class="mg-kv-card"><div class="mkv-k">Components</div><div class="mkv-v">${comps.length}</div></div>
|
|
7889
8646
|
<div class="mg-kv-card"><div class="mkv-k">Largest</div><div class="mkv-v">${comps[0] ? comps[0].length : 0}</div></div>
|
|
7890
8647
|
</div>` +
|
|
7891
|
-
top5.map((c, i) => `<div style="margin-bottom:8px"><div style="font-size:11px;color:var(--text-xs);margin-bottom:3px">Component ${i+1} — ${c.length} nodes</div><div style="font-size:11px;font-family:var(--mono);color:var(--text-mid)">${c.slice(0,5).map(esc).join(', ')}${c.length > 5 ? ` … +${c.length-5} more` : ''}</div></div>`).join('')
|
|
8648
|
+
top5.map((c, i) => `<div style="margin-bottom:8px"><div style="font-size:11px;color:var(--text-xs);margin-bottom:3px">Component ${i+1} — ${c.length} nodes</div><div style="font-size:11px;font-family:var(--mono);color:var(--text-mid)">${c.slice(0,5).map(esc).join(', ')}${c.length > 5 ? ` … +${c.length-5} more` : ''}</div></div>`).join('') +
|
|
8649
|
+
(comps.length > 5 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Showing 5 of ${comps.length} components</div>` : '');
|
|
7892
8650
|
|
|
7893
8651
|
} else if (id === 'topo') {
|
|
7894
8652
|
const inDeg = {}; const adj = {};
|
|
@@ -7965,14 +8723,16 @@ function mgRunClient(id) {
|
|
|
7965
8723
|
|
|
7966
8724
|
} else if (id === 'betweenness') {
|
|
7967
8725
|
const bc = mgApproxBetweenness(g);
|
|
7968
|
-
const
|
|
8726
|
+
const bcAll = Object.entries(bc).sort((a,b) => b[1]-a[1]);
|
|
8727
|
+
const bcRanked = bcAll.slice(0,20);
|
|
7969
8728
|
const maxBC = bcRanked.length ? bcRanked[0][1] : 1;
|
|
7970
8729
|
if (!bcRanked.length || maxBC === 0) { html = '<div class="loading-txt">Not enough graph data for betweenness</div>'; }
|
|
7971
8730
|
else {
|
|
7972
8731
|
html = `<div style="font-size:11px;color:var(--text-xs);margin-bottom:8px">Approximate betweenness centrality (sample-based BFS). High score = architectural bridge.</div>
|
|
7973
8732
|
<table class="mg-table"><thead><tr><th>#</th><th>Node</th><th>Score</th><th style="width:160px">Weight</th></tr></thead><tbody>` +
|
|
7974
8733
|
bcRanked.map(([k, v], i) => `<tr><td>${i+1}</td><td title="${esc(k)}">${esc(mgShortLabel(k))}</td><td>${v.toFixed(1)}</td><td><div class="mg-bar-track" style="height:8px"><div class="mg-bar-fill mg" style="width:${Math.round((v/maxBC)*100)}%"></div></div></td></tr>`).join('') +
|
|
7975
|
-
`</tbody></table
|
|
8734
|
+
`</tbody></table>` +
|
|
8735
|
+
(bcAll.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${bcAll.length} nodes</div>` : '');
|
|
7976
8736
|
}
|
|
7977
8737
|
|
|
7978
8738
|
} else if (id === 'jaccard') {
|
|
@@ -8095,7 +8855,7 @@ function mgRenderAnalysisResult(toolId, text) {
|
|
|
8095
8855
|
return `<div class="mgr-ranked-row">
|
|
8096
8856
|
<span class="mgr-ranked-num">${r.rank}</span>
|
|
8097
8857
|
<span class="mgr-ranked-name" title="${h(r.name)}">${h(r.name)}</span>
|
|
8098
|
-
<div class="mgr-ranked-bar-wrap"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
|
|
8858
|
+
<div class="mgr-ranked-bar-wrap" title="${h(r.name)}: ${h(String(r.val))}"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
|
|
8099
8859
|
<span class="mgr-ranked-val">${h(String(r.val))}</span>
|
|
8100
8860
|
</div>`;
|
|
8101
8861
|
}).join('')}</div>`;
|
|
@@ -8225,7 +8985,7 @@ function mgRenderAnalysisResult(toolId, text) {
|
|
|
8225
8985
|
return `<div class="mgr-ranked-row">
|
|
8226
8986
|
<span class="mgr-ranked-num">${r.rank}</span>
|
|
8227
8987
|
<span class="mgr-ranked-name" title="${h(r.path)}">${h(name)}</span>
|
|
8228
|
-
<div class="mgr-ranked-bar-wrap"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
|
|
8988
|
+
<div class="mgr-ranked-bar-wrap" title="${h(r.path)}: ${r.lines.toLocaleString()} lines"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
|
|
8229
8989
|
<span class="mgr-ranked-val">${r.lines.toLocaleString()}</span>
|
|
8230
8990
|
</div>`;
|
|
8231
8991
|
}).join('')}</div>`;
|
|
@@ -8273,6 +9033,13 @@ async function mgRunServer(id) {
|
|
|
8273
9033
|
}
|
|
8274
9034
|
|
|
8275
9035
|
// ── QUERY TAB ──────────────────────────────────────────────
|
|
9036
|
+
// Click any query result box to copy its text content
|
|
9037
|
+
document.addEventListener('click', e => {
|
|
9038
|
+
const el = e.target.closest('.mg-query-result');
|
|
9039
|
+
if (!el || !el.textContent.trim()) return;
|
|
9040
|
+
navigator.clipboard.writeText(el.textContent).then(() => showToast('Copied', 'Query result copied', 'ok')).catch(() => {});
|
|
9041
|
+
});
|
|
9042
|
+
|
|
8276
9043
|
function mgQuerySearch(q) {
|
|
8277
9044
|
const res = document.getElementById('mg-q-search-result');
|
|
8278
9045
|
if (!q.trim()) { res.style.display = 'none'; return; }
|
|
@@ -8467,7 +9234,7 @@ function mgRenderExport() {
|
|
|
8467
9234
|
<div class="mg-export-card">
|
|
8468
9235
|
<div class="mec-name">${esc(x.name)}</div>
|
|
8469
9236
|
<div class="mec-desc">${esc(x.desc)}</div>
|
|
8470
|
-
<button class="btn" onclick="mgExport('${esc(x.id)}')" style="margin-top:auto">EXPORT</button>
|
|
9237
|
+
<button class="btn" title="Export graph as ${esc(x.name)}" onclick="mgExport('${esc(x.id)}')" style="margin-top:auto">EXPORT</button>
|
|
8471
9238
|
</div>`).join('');
|
|
8472
9239
|
}
|
|
8473
9240
|
|
|
@@ -8731,8 +9498,8 @@ function mgRenderWiki() {
|
|
|
8731
9498
|
// type pills
|
|
8732
9499
|
const types = [...new Set(nodes.map(n => n.type || n.kind || 'unknown'))].sort();
|
|
8733
9500
|
const pillsEl = document.getElementById('mg-wiki-pills');
|
|
8734
|
-
pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" onclick="mgWikiFilterType('all')">All</span>` +
|
|
8735
|
-
types.map(t => `<span class="mg-pill" data-type="${esc(t)}" onclick="mgWikiFilterType('${esc(t)}')">${esc(t)}</span>`).join('');
|
|
9501
|
+
pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" title="Show all node types" onclick="mgWikiFilterType('all')">All</span>` +
|
|
9502
|
+
types.map(t => `<span class="mg-pill" data-type="${esc(t)}" title="Filter by type: ${esc(t)}" onclick="mgWikiFilterType('${esc(t)}')">${esc(t)}</span>`).join('');
|
|
8736
9503
|
|
|
8737
9504
|
mgRenderWikiList('');
|
|
8738
9505
|
}
|
|
@@ -8803,9 +9570,11 @@ function mgWikiSearchDebounced(q) {
|
|
|
8803
9570
|
apiFetch('/api/monograph-wiki-search?q=' + enc(q) + '&dir=' + enc(DIR))
|
|
8804
9571
|
.then(d => {
|
|
8805
9572
|
if (!d || !d.nodes || !d.nodes.length) return;
|
|
8806
|
-
|
|
9573
|
+
const serverNodes = d.nodes.slice(0, 50);
|
|
9574
|
+
el._wikiData = serverNodes;
|
|
9575
|
+
el.innerHTML = serverNodes.map((n, idx) => {
|
|
8807
9576
|
const lbl = n.label || n.name || '';
|
|
8808
|
-
return `<div class="mg-node-item"><div class="mni-ico">${mgNodeIcon(n.type)}</div><div class="mni-lbl">${esc(lbl)}</div><div class="mni-path">${esc(mgShortPath(lbl))}</div><div class="mni-deg">${n.degree || 0}</div></div>`;
|
|
9577
|
+
return `<div class="mg-node-item" onclick="mgWikiShowDetail(${idx})"><div class="mni-ico">${mgNodeIcon(n.type)}</div><div class="mni-lbl">${esc(lbl)}</div><div class="mni-path">${esc(mgShortPath(lbl))}</div><div class="mni-deg">${n.degree || 0}</div></div>`;
|
|
8809
9578
|
}).join('');
|
|
8810
9579
|
}).catch(() => {});
|
|
8811
9580
|
}
|
|
@@ -8830,9 +9599,9 @@ function mgWikiShowDetail(idx) {
|
|
|
8830
9599
|
</div>
|
|
8831
9600
|
${contentPreview ? `<div class="mdp-content-preview">${esc(contentPreview)}</div>` : ''}
|
|
8832
9601
|
<div class="mdp-actions">
|
|
8833
|
-
${contentPreview ? `<button class="btn" id="mg-copy-btn" data-copy-idx="${idx}">COPY CONTENT</button>` : ''}
|
|
8834
|
-
<button class="btn" data-explain-idx="${idx}">EXPLAIN</button>
|
|
8835
|
-
<button class="btn" data-related-idx="${idx}">FIND RELATED</button>
|
|
9602
|
+
${contentPreview ? `<button class="btn" id="mg-copy-btn" title="Copy file content to clipboard" data-copy-idx="${idx}">COPY CONTENT</button>` : ''}
|
|
9603
|
+
<button class="btn" title="Ask the AI to explain this node" data-explain-idx="${idx}">EXPLAIN</button>
|
|
9604
|
+
<button class="btn" title="Find nodes related to this one in the graph" data-related-idx="${idx}">FIND RELATED</button>
|
|
8836
9605
|
</div>
|
|
8837
9606
|
<div class="mdp-explain-result" id="mg-explain-result" style="display:none"></div>
|
|
8838
9607
|
<div id="mg-related-result" style="display:none;margin-top:10px"></div>`;
|
|
@@ -8867,6 +9636,26 @@ async function mgWikiExplain(nodeId, btn) {
|
|
|
8867
9636
|
btn.disabled = false; btn.textContent = 'EXPLAIN';
|
|
8868
9637
|
}
|
|
8869
9638
|
|
|
9639
|
+
function mgWikiJumpToNode(nodeId) {
|
|
9640
|
+
if (!_mgGraph) return;
|
|
9641
|
+
const el = document.getElementById('mg-wiki-list');
|
|
9642
|
+
const data = el._wikiData || (_mgGraph.nodes || []);
|
|
9643
|
+
const idx = data.findIndex(n => (n.id || n.name || n.label || '') === nodeId);
|
|
9644
|
+
if (idx >= 0) {
|
|
9645
|
+
mgWikiShowDetail(idx);
|
|
9646
|
+
document.getElementById('mg-wiki-detail')?.scrollIntoView({ behavior:'smooth', block:'nearest' });
|
|
9647
|
+
} else {
|
|
9648
|
+
// node not in current filtered list — search for it and jump
|
|
9649
|
+
const searchEl = document.getElementById('mg-wiki-search');
|
|
9650
|
+
if (searchEl) { searchEl.value = nodeId; mgRenderWikiList(nodeId); }
|
|
9651
|
+
setTimeout(() => {
|
|
9652
|
+
const newData = document.getElementById('mg-wiki-list')?._wikiData || [];
|
|
9653
|
+
const newIdx = newData.findIndex(n => (n.id || n.name || n.label || '') === nodeId);
|
|
9654
|
+
if (newIdx >= 0) mgWikiShowDetail(newIdx);
|
|
9655
|
+
}, 100);
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
|
|
8870
9659
|
function mgWikiFindRelated(nodeId) {
|
|
8871
9660
|
const el = document.getElementById('mg-related-result');
|
|
8872
9661
|
if (!el || !_mgGraph) return;
|
|
@@ -8882,11 +9671,12 @@ function mgWikiFindRelated(nodeId) {
|
|
|
8882
9671
|
hop1.forEach(v => { (adj[v] ? [...adj[v]] : []).forEach(w => { if (w !== nodeId && !hop1.includes(w)) hop2.add(w); }); });
|
|
8883
9672
|
const related = [...hop1, ...hop2].slice(0, 20);
|
|
8884
9673
|
if (!related.length) { el.innerHTML = '<div class="loading-txt">No related nodes found</div>'; el.style.display = 'block'; return; }
|
|
8885
|
-
el.innerHTML = `<div style="font-size:11px;color:var(--text-xs);margin-bottom:6px">Related nodes (2-hop neighborhood)</div>` +
|
|
9674
|
+
el.innerHTML = `<div style="font-size:11px;color:var(--text-xs);margin-bottom:6px">Related nodes (2-hop neighborhood) — click to open</div>` +
|
|
8886
9675
|
`<ul class="mg-node-list">` + related.map(k => {
|
|
8887
9676
|
const n = (_mgGraph.nodes || []).find(x => (x.id || x.name || x.label || '') === k);
|
|
8888
9677
|
const lbl = n ? (n.label || n.name || k) : k;
|
|
8889
|
-
|
|
9678
|
+
const typeIcon = n ? mgNodeIcon(n.type || n.kind) : '◈';
|
|
9679
|
+
return `<li style="cursor:pointer;display:flex;align-items:center;gap:6px;padding:2px 0" onclick="mgWikiJumpToNode(${JSON.stringify(k)})" title="Open ${esc(lbl)}"><span style="opacity:0.6">${typeIcon}</span>${esc(lbl)}</li>`;
|
|
8890
9680
|
}).join('') + `</ul>`;
|
|
8891
9681
|
el.style.display = 'block';
|
|
8892
9682
|
}
|
|
@@ -8902,18 +9692,19 @@ async function mgRebuildDocs() {
|
|
|
8902
9692
|
btn.disabled = true; btn.textContent = 'BUILDING…';
|
|
8903
9693
|
try {
|
|
8904
9694
|
await fetch('/api/monograph-build-docs?dir=' + enc(DIR), { method:'POST' });
|
|
9695
|
+
showToast('Building', 'Documentation build started…', 'info');
|
|
8905
9696
|
// poll until done (max 60 attempts = ~2 minutes)
|
|
8906
9697
|
let polls = 0;
|
|
8907
9698
|
const poll = async () => {
|
|
8908
|
-
if (++polls > 60) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; return; }
|
|
9699
|
+
if (++polls > 60) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; showToast('Timeout', 'Doc build is taking longer than expected', 'warn'); return; }
|
|
8909
9700
|
try {
|
|
8910
9701
|
const d = await apiFetch('/api/monograph-build-docs-status?dir=' + enc(DIR));
|
|
8911
|
-
if (d && d.done) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; mgWikiRefresh(); return; }
|
|
9702
|
+
if (d && d.done) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; mgWikiRefresh(); showToast('Done', 'Documentation built', 'ok'); return; }
|
|
8912
9703
|
} catch (_) {}
|
|
8913
9704
|
setTimeout(poll, 2000);
|
|
8914
9705
|
};
|
|
8915
9706
|
poll();
|
|
8916
|
-
} catch (e) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; }
|
|
9707
|
+
} catch (e) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; showToast('Error', e.message, 'err'); }
|
|
8917
9708
|
}
|
|
8918
9709
|
|
|
8919
9710
|
// ── memory CRUD ────────────────────────────────────────────
|
|
@@ -8951,6 +9742,41 @@ async function loadMemoriesTab() {
|
|
|
8951
9742
|
}
|
|
8952
9743
|
}
|
|
8953
9744
|
|
|
9745
|
+
function filterMemList(q) {
|
|
9746
|
+
const lq = (q || '').toLowerCase();
|
|
9747
|
+
let totalVisible = 0;
|
|
9748
|
+
document.querySelectorAll('#mem-list-pane .mem-item').forEach(el => {
|
|
9749
|
+
const text = (el.textContent || '').toLowerCase();
|
|
9750
|
+
const show = !lq || text.includes(lq);
|
|
9751
|
+
el.style.display = show ? '' : 'none';
|
|
9752
|
+
if (show) totalVisible++;
|
|
9753
|
+
});
|
|
9754
|
+
document.querySelectorAll('#mem-list-pane .mem-type-hdr').forEach(hdr => {
|
|
9755
|
+
let next = hdr.nextElementSibling;
|
|
9756
|
+
let anyVisible = false;
|
|
9757
|
+
while (next && !next.classList.contains('mem-type-hdr')) {
|
|
9758
|
+
if (next.style.display !== 'none') anyVisible = true;
|
|
9759
|
+
next = next.nextElementSibling;
|
|
9760
|
+
}
|
|
9761
|
+
hdr.style.display = anyVisible ? '' : 'none';
|
|
9762
|
+
});
|
|
9763
|
+
const list = document.getElementById('mem-list-pane');
|
|
9764
|
+
const noRes = document.getElementById('mem-filter-noresult');
|
|
9765
|
+
if (lq && totalVisible === 0 && _memFiles.length > 0) {
|
|
9766
|
+
if (!noRes) {
|
|
9767
|
+
const el = document.createElement('div');
|
|
9768
|
+
el.id = 'mem-filter-noresult';
|
|
9769
|
+
el.style.cssText = 'padding:12px 14px;color:var(--text-lo);font-size:12px';
|
|
9770
|
+
el.textContent = 'No memories match "' + q.slice(0, 30) + '"';
|
|
9771
|
+
list.appendChild(el);
|
|
9772
|
+
} else {
|
|
9773
|
+
noRes.textContent = 'No memories match "' + q.slice(0, 30) + '"';
|
|
9774
|
+
}
|
|
9775
|
+
} else if (noRes) {
|
|
9776
|
+
noRes.remove();
|
|
9777
|
+
}
|
|
9778
|
+
}
|
|
9779
|
+
|
|
8954
9780
|
function _renderMemList() {
|
|
8955
9781
|
const list = document.getElementById('mem-list-pane');
|
|
8956
9782
|
if (!list) return;
|
|
@@ -8966,7 +9792,8 @@ function _renderMemList() {
|
|
|
8966
9792
|
const col = _MEM_COLORS[type] || _MEM_COLOR_FALLBACK;
|
|
8967
9793
|
const fname = f.filename || f.name || '?';
|
|
8968
9794
|
const active = _selMemFilename === fname ? ' active' : '';
|
|
8969
|
-
|
|
9795
|
+
const memTitle = [f.description, f.name || fname].filter(Boolean).join(' — ');
|
|
9796
|
+
return '<div class="mem-item' + active + '" data-filename="' + esc(fname) + '" onclick="selectMem(this.dataset.filename)" title="' + esc(memTitle) + '">' +
|
|
8970
9797
|
'<span class="mem-type-dot" style="background:' + esc(col) + ';flex-shrink:0"></span>' +
|
|
8971
9798
|
'<span class="mem-item-name">' + esc(f.name || fname.replace('.md', '')) + '</span>' +
|
|
8972
9799
|
'</div>';
|
|
@@ -8985,17 +9812,17 @@ function selectMem(filename) {
|
|
|
8985
9812
|
const rawBody = f.body || f.content || '';
|
|
8986
9813
|
const bodyHtml = rawBody
|
|
8987
9814
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
8988
|
-
.replace(/\*\*(.+?)\*\*/g, (_, g) => '<strong>' +
|
|
8989
|
-
.replace(/^#{1,3} (.+)/gm, (_, g) => '<div style="font-weight:600;color:var(--text-hi);margin:6px 0 2px">' +
|
|
8990
|
-
.replace(/^- (.+)/gm, (_, g) => '<div style="padding-left:12px">• ' +
|
|
9815
|
+
.replace(/\*\*(.+?)\*\*/g, (_, g) => '<strong>' + g + '</strong>')
|
|
9816
|
+
.replace(/^#{1,3} (.+)/gm, (_, g) => '<div style="font-weight:600;color:var(--text-hi);margin:6px 0 2px">' + g + '</div>')
|
|
9817
|
+
.replace(/^- (.+)/gm, (_, g) => '<div style="padding-left:12px">• ' + g + '</div>');
|
|
8991
9818
|
const srcBadge = f.source === 'backend'
|
|
8992
9819
|
? '<span class="mem-badge" style="background:var(--text-lo)22;color:var(--text-lo);margin-left:6px" title="Stored in the AgentDB backend store, not as a file">backend</span>'
|
|
8993
9820
|
: '';
|
|
8994
9821
|
const actions = (f.readonly || f.source === 'backend')
|
|
8995
9822
|
? '<div class="mem-actions"><span style="font-size:11px;color:var(--text-lo)">Read-only — stored in the backend memory store (not a file)</span></div>'
|
|
8996
9823
|
: '<div class="mem-actions">' +
|
|
8997
|
-
'<button class="btn" onclick="openEditMemModal(' + JSON.stringify(filename) + ')">✎ Edit</button>' +
|
|
8998
|
-
'<button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">✕ Delete</button>' +
|
|
9824
|
+
'<button class="btn" title="Edit this memory entry" onclick="openEditMemModal(' + JSON.stringify(filename) + ')">✎ Edit</button>' +
|
|
9825
|
+
'<button class="btn" title="Delete this memory entry" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">✕ Delete</button>' +
|
|
8999
9826
|
'</div>';
|
|
9000
9827
|
detail.innerHTML =
|
|
9001
9828
|
'<span class="mem-badge" style="background:' + col + '22;color:' + col + '">' + esc(f.type || '?') + '</span>' + srcBadge +
|
|
@@ -9105,7 +9932,7 @@ function _renderSwarmRunList() {
|
|
|
9105
9932
|
'<span class="swarm-topo-pill">' + esc(topo) + '</span>' +
|
|
9106
9933
|
(live ? '<span class="swarm-live">⬤ LIVE</span>' : '') +
|
|
9107
9934
|
'</div>' +
|
|
9108
|
-
'<div style="font-size:11px;color:var(--text-lo);margin-top:2px">' + (r.agentCount || 0) + ' agents · ' + relTime(r.startedAt
|
|
9935
|
+
'<div style="font-size:11px;color:var(--text-lo);margin-top:2px">' + (r.agentCount || 0) + ' agents · ' + (function(ts){const d=ts&&new Date(typeof ts==='number'?ts:Number(ts)||ts);return d&&!isNaN(d)?'<span title="'+d.toLocaleString()+'">'+relTime(ts)+'</span>':relTime(ts);})(r.startedAt||r.created_at) + '</div>' +
|
|
9109
9936
|
'</div>';
|
|
9110
9937
|
}).join('');
|
|
9111
9938
|
}
|
|
@@ -9118,7 +9945,7 @@ async function selectSwarmRun(idx) {
|
|
|
9118
9945
|
if (!detail) return;
|
|
9119
9946
|
detail.innerHTML =
|
|
9120
9947
|
'<div style="margin-bottom:10px">' +
|
|
9121
|
-
'<div style="font-size:13px;font-weight:600;color:var(--text-hi)">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
|
|
9948
|
+
'<div style="font-size:13px;font-weight:600;color:var(--text-hi)" title="' + esc((run.swarmId || run.id || '').toString()) + '">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
|
|
9122
9949
|
'<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + esc(run.consensus || '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
|
|
9123
9950
|
'</div>' +
|
|
9124
9951
|
'<canvas id="swarm-topo-canvas" style="width:100%;max-width:380px;height:190px;display:block;border:1px solid var(--border);border-radius:6px;margin-bottom:12px"></canvas>' +
|
|
@@ -9195,14 +10022,16 @@ function _renderSwarmAgents(run) {
|
|
|
9195
10022
|
if (!el) return;
|
|
9196
10023
|
const agents = run.agents || [];
|
|
9197
10024
|
if (!agents.length) { el.innerHTML = ''; return; }
|
|
10025
|
+
const shownA = agents.slice(0, 15);
|
|
9198
10026
|
el.innerHTML = '<div class="m-group-title" style="margin-bottom:4px">Agents</div>' +
|
|
9199
|
-
|
|
10027
|
+
shownA.map(a =>
|
|
9200
10028
|
'<div style="display:flex;gap:10px;padding:3px 0;font-size:11px;border-bottom:1px solid var(--border)">' +
|
|
9201
|
-
'<span style="color:var(--text-lo);font-family:var(--mono);min-width:70px">' + esc((a.id || '').toString().slice(0, 10)) + '</span>' +
|
|
10029
|
+
'<span style="color:var(--text-lo);font-family:var(--mono);min-width:70px" title="' + esc((a.id || '').toString()) + '">' + esc((a.id || '').toString().slice(0, 10)) + ((a.id || '').toString().length > 10 ? '…' : '') + '</span>' +
|
|
9202
10030
|
'<span style="color:var(--text-hi)">' + esc(a.type || a.role || 'worker') + '</span>' +
|
|
9203
10031
|
'<span style="margin-left:auto;color:var(--text-xs)">' + esc(a.status || '—') + '</span>' +
|
|
9204
10032
|
'</div>'
|
|
9205
|
-
).join('')
|
|
10033
|
+
).join('') +
|
|
10034
|
+
(agents.length > 15 ? '<div style="font-size:11px;color:var(--text-xs);padding:3px 0">+' + (agents.length - 15) + ' more agents</div>' : '');
|
|
9206
10035
|
}
|
|
9207
10036
|
|
|
9208
10037
|
async function _loadSwarmEvents(swarmId) {
|
|
@@ -9213,11 +10042,13 @@ async function _loadSwarmEvents(swarmId) {
|
|
|
9213
10042
|
const data = await apiFetch('/api/swarm-events?agentId=' + enc(swarmId) + '&dir=' + enc(DIR));
|
|
9214
10043
|
const events = Array.isArray(data) ? data : (data.events || []);
|
|
9215
10044
|
if (!events.length) { el.innerHTML = ''; return; }
|
|
9216
|
-
|
|
9217
|
-
|
|
10045
|
+
const shownEv = events.slice(-40);
|
|
10046
|
+
el.innerHTML = '<div class="m-group-title" style="margin-bottom:3px">Events' + (events.length > 40 ? '<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:6px">last 40 of ' + events.length + '</span>' : '') + '</div>' +
|
|
10047
|
+
shownEv.map(e =>
|
|
9218
10048
|
'<div style="color:var(--text-lo);padding:2px 0;border-bottom:1px solid var(--border)">' +
|
|
9219
|
-
|
|
9220
|
-
esc(
|
|
10049
|
+
'<span title="' + ((e.ts || e.timestamp) ? new Date(typeof (e.ts || e.timestamp) === 'number' ? (e.ts || e.timestamp) : (e.ts || e.timestamp)).toLocaleString() : '') + '">' + esc(relTime(e.ts || e.timestamp)) + '</span>' +
|
|
10050
|
+
' <span style="color:var(--text-mid)">' + esc(e.type || e.kind || '?') + '</span> ' +
|
|
10051
|
+
'<span title="' + esc((e.message || e.data || '').toString()) + '">' + esc((e.message || e.data || '').toString().slice(0, 70)) + ((e.message || e.data || '').toString().length > 70 ? '…' : '') + '</span>' +
|
|
9221
10052
|
'</div>'
|
|
9222
10053
|
).join('');
|
|
9223
10054
|
el.scrollTop = el.scrollHeight;
|
|
@@ -9281,23 +10112,25 @@ function _renderChunks(list) {
|
|
|
9281
10112
|
grid.innerHTML = '<div class="empty">No chunks indexed.<br><span style="font-size:11px;color:var(--text-xs)">Run /monomind:understand to build the index.</span></div>';
|
|
9282
10113
|
return;
|
|
9283
10114
|
}
|
|
9284
|
-
|
|
10115
|
+
const shown = list.slice(0, 200);
|
|
10116
|
+
grid.innerHTML = shown.map(c => {
|
|
9285
10117
|
const src = (c.source || c.file || '').split('/').slice(-2).join('/');
|
|
9286
10118
|
const excerpt = (c.content || c.text || c.body || '').slice(0, 220);
|
|
9287
10119
|
const ns = c.namespace || c.type || '';
|
|
9288
|
-
const chunkId = JSON.stringify(c.id || c.path || c.source || '');
|
|
9289
|
-
const chunkContent = JSON.stringify(c.content || c.text || c.body || '');
|
|
9290
|
-
const chunkSrc = JSON.stringify(src);
|
|
10120
|
+
const chunkId = JSON.stringify(c.id || c.path || c.source || '').replace(/"/g, '"');
|
|
10121
|
+
const chunkContent = JSON.stringify(c.content || c.text || c.body || '').replace(/"/g, '"');
|
|
10122
|
+
const chunkSrc = JSON.stringify(src).replace(/"/g, '"');
|
|
9291
10123
|
return '<div class="chunk-card" data-search="' + esc((src + ' ' + excerpt + ' ' + ns).toLowerCase()) + '">' +
|
|
9292
10124
|
'<div class="chunk-src">' + esc(src || '—') + '</div>' +
|
|
9293
|
-
'<div class="chunk-excerpt">' + esc(excerpt) + '</div>' +
|
|
10125
|
+
'<div class="chunk-excerpt" title="' + esc(c.content || c.text || c.body || '') + '">' + esc(excerpt) + ((c.content || c.text || c.body || '').length > 220 ? '…' : '') + '</div>' +
|
|
9294
10126
|
'<div class="chunk-footer">' +
|
|
9295
10127
|
(ns ? '<span class="chunk-ns">' + esc(ns) + '</span>' : '') +
|
|
9296
|
-
'<button class="btn" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
|
|
9297
|
-
'<button class="btn" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunk(' + chunkId + ')">✕</button>' +
|
|
10128
|
+
'<button class="btn" title="Edit this chunk" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
|
|
10129
|
+
'<button class="btn" title="Delete this chunk" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunk(' + chunkId + ')">✕</button>' +
|
|
9298
10130
|
'</div>' +
|
|
9299
10131
|
'</div>';
|
|
9300
|
-
}).join('')
|
|
10132
|
+
}).join('') +
|
|
10133
|
+
(list.length > 200 ? '<div style="font-size:11px;color:var(--text-xs);padding:8px 0;text-align:center">Showing 200 of ' + list.length + ' chunks — use the filter to narrow results</div>' : '');
|
|
9301
10134
|
}
|
|
9302
10135
|
|
|
9303
10136
|
function openChunkEdit(id, content, srcLabel) {
|
|
@@ -9371,9 +10204,33 @@ async function buildKnowledgeDocs() {
|
|
|
9371
10204
|
|
|
9372
10205
|
function filterChunks(q) {
|
|
9373
10206
|
const lq = q.toLowerCase();
|
|
9374
|
-
document.querySelectorAll('#chunks-grid .chunk-card')
|
|
9375
|
-
|
|
10207
|
+
const cards = document.querySelectorAll('#chunks-grid .chunk-card');
|
|
10208
|
+
let visible = 0;
|
|
10209
|
+
cards.forEach(el => {
|
|
10210
|
+
const show = !lq || (el.dataset.search || '').includes(lq);
|
|
10211
|
+
el.style.display = show ? '' : 'none';
|
|
10212
|
+
if (show) visible++;
|
|
9376
10213
|
});
|
|
10214
|
+
const countEl = document.getElementById('chunk-count-val');
|
|
10215
|
+
if (countEl && lq) countEl.textContent = visible + ' / ' + cards.length;
|
|
10216
|
+
else if (countEl) countEl.textContent = cards.length.toLocaleString();
|
|
10217
|
+
// zero-results empty state
|
|
10218
|
+
const grid = document.getElementById('chunks-grid');
|
|
10219
|
+
const noRes = document.getElementById('chunks-filter-noresult');
|
|
10220
|
+
if (lq && visible === 0 && cards.length > 0) {
|
|
10221
|
+
if (!noRes) {
|
|
10222
|
+
const el = document.createElement('div');
|
|
10223
|
+
el.id = 'chunks-filter-noresult';
|
|
10224
|
+
el.className = 'empty';
|
|
10225
|
+
el.style.cssText = 'padding:20px 0;font-size:13px';
|
|
10226
|
+
el.textContent = 'No chunks match "' + q.slice(0, 40) + '"';
|
|
10227
|
+
grid.appendChild(el);
|
|
10228
|
+
} else {
|
|
10229
|
+
noRes.textContent = 'No chunks match "' + q.slice(0, 40) + '"';
|
|
10230
|
+
}
|
|
10231
|
+
} else if (noRes) {
|
|
10232
|
+
noRes.remove();
|
|
10233
|
+
}
|
|
9377
10234
|
}
|
|
9378
10235
|
|
|
9379
10236
|
async function deleteChunk(id) {
|
|
@@ -9400,8 +10257,28 @@ async function loadAgentGraphTab() {
|
|
|
9400
10257
|
const bar = document.getElementById('ag-summary-bar');
|
|
9401
10258
|
if (bar) bar.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
9402
10259
|
try {
|
|
9403
|
-
const
|
|
9404
|
-
|
|
10260
|
+
const raw = await apiFetch('/api/graph?dir=' + enc(DIR));
|
|
10261
|
+
// API returns { nodes, edges } — normalize into the shape renderers expect
|
|
10262
|
+
const sesNodes = (raw.nodes || []).filter(n => n.type === 'session');
|
|
10263
|
+
const agNodes = (raw.nodes || []).filter(n => n.type === 'agenttype');
|
|
10264
|
+
const sessions = sesNodes.map(n => ({
|
|
10265
|
+
id: n.id,
|
|
10266
|
+
file: n.id,
|
|
10267
|
+
turns: n.turns || 0,
|
|
10268
|
+
toolCount: n.totalTools || 0,
|
|
10269
|
+
spawnCount: Object.values(n.agentSpawns || {}).reduce((a, b) => a + b, 0),
|
|
10270
|
+
cost: n.cost || 0,
|
|
10271
|
+
agentTypes: n.agentSpawns || {},
|
|
10272
|
+
tools: n.toolCounts || {},
|
|
10273
|
+
}));
|
|
10274
|
+
_agData = {
|
|
10275
|
+
sessions,
|
|
10276
|
+
sessionCount: sessions.length,
|
|
10277
|
+
agentTypes: agNodes.length,
|
|
10278
|
+
totalSpawns: agNodes.reduce((a, n) => a + (n.totalSpawns || 0), 0),
|
|
10279
|
+
totalToolCalls: sesNodes.reduce((a, n) => a + (n.totalTools || 0), 0),
|
|
10280
|
+
totalCost: sesNodes.reduce((a, n) => a + (n.cost || 0), 0),
|
|
10281
|
+
};
|
|
9405
10282
|
_renderAgSummary();
|
|
9406
10283
|
_renderAgSessList();
|
|
9407
10284
|
} catch (e) {
|
|
@@ -9432,15 +10309,27 @@ function _renderAgSessList() {
|
|
|
9432
10309
|
const el = document.getElementById('ag-sess-list');
|
|
9433
10310
|
if (!_agData || !el) return;
|
|
9434
10311
|
const sessions = _agData.sessions || [];
|
|
9435
|
-
if (!sessions.length) { el.innerHTML = '<div class="empty" style="font-size:12px">No sessions
|
|
9436
|
-
el.innerHTML =
|
|
9437
|
-
'<
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
'
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
10312
|
+
if (!sessions.length) { el.innerHTML = '<div class="empty" style="font-size:12px">No sessions<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Sessions are recorded when Claude Code runs inside this project.</div></div>'; return; }
|
|
10313
|
+
el.innerHTML =
|
|
10314
|
+
'<input class="filter-input" id="ag-sess-filter" type="text" placeholder="Filter sessions…" style="width:100%;margin-bottom:6px;font-size:11px;box-sizing:border-box" oninput="filterAgSessions(this.value)" title="Filter by session ID">' +
|
|
10315
|
+
'<div id="ag-sess-rows">' +
|
|
10316
|
+
sessions.map((s, i) =>
|
|
10317
|
+
'<div class="sess-row" style="margin-bottom:4px" data-sid="' + esc(s.id || s.file || '') + '" onclick="selectAgSession(' + i + ')">' +
|
|
10318
|
+
'<div class="sr-top">' +
|
|
10319
|
+
'<div class="sr-prompt" style="font-size:11px" title="' + esc(s.id || s.file || '') + '">' + esc((s.id || s.file || '').slice(-16)) + '</div>' +
|
|
10320
|
+
'</div>' +
|
|
10321
|
+
'<div style="font-size:10px;color:var(--text-lo);margin-top:1px">' + (s.spawnCount || 0) + ' spawns · ' + (s.toolCount || 0) + ' tools</div>' +
|
|
10322
|
+
'</div>'
|
|
10323
|
+
).join('') +
|
|
10324
|
+
'</div>';
|
|
10325
|
+
}
|
|
10326
|
+
|
|
10327
|
+
function filterAgSessions(q) {
|
|
10328
|
+
const lq = (q || '').toLowerCase();
|
|
10329
|
+
document.querySelectorAll('#ag-sess-rows .sess-row').forEach(el => {
|
|
10330
|
+
const sid = (el.dataset.sid || '').toLowerCase();
|
|
10331
|
+
el.style.display = (!lq || sid.includes(lq)) ? '' : 'none';
|
|
10332
|
+
});
|
|
9444
10333
|
}
|
|
9445
10334
|
|
|
9446
10335
|
function selectAgSession(idx) {
|
|
@@ -9449,8 +10338,10 @@ function selectAgSession(idx) {
|
|
|
9449
10338
|
document.querySelectorAll('#ag-sess-list .sess-row').forEach((el, i) => el.classList.toggle('active', i === idx));
|
|
9450
10339
|
const detail = document.getElementById('ag-detail');
|
|
9451
10340
|
if (!detail) return;
|
|
9452
|
-
const
|
|
9453
|
-
const
|
|
10341
|
+
const agAllArr = Object.entries(s.agentTypes || {}).sort((a, b) => b[1] - a[1]);
|
|
10342
|
+
const toolAllArr = Object.entries(s.tools || {}).sort((a, b) => b[1] - a[1]);
|
|
10343
|
+
const agArr = agAllArr.slice(0, 12);
|
|
10344
|
+
const toolArr = toolAllArr.slice(0, 15);
|
|
9454
10345
|
const maxAg = agArr.length ? Math.max(...agArr.map(x => x[1])) : 1;
|
|
9455
10346
|
const maxTool = toolArr.length ? Math.max(...toolArr.map(x => x[1])) : 1;
|
|
9456
10347
|
detail.innerHTML =
|
|
@@ -9460,20 +10351,20 @@ function selectAgSession(idx) {
|
|
|
9460
10351
|
'<span>Tools: <b>' + (s.toolCount || 0) + '</b></span>' +
|
|
9461
10352
|
(s.cost != null ? '<span style="color:var(--accent)">$' + Number(s.cost).toFixed(4) + '</span>' : '') +
|
|
9462
10353
|
'</div>' +
|
|
9463
|
-
(agArr.length ? '<div class="m-group-title" style="margin-bottom:5px">Agent Types</div>' +
|
|
10354
|
+
(agArr.length ? '<div class="m-group-title" style="margin-bottom:5px">Agent Types' + (agAllArr.length > 12 ? '<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:6px">top 12 of ' + agAllArr.length + '</span>' : '') + '</div>' +
|
|
9464
10355
|
agArr.map(function(entry) {
|
|
9465
10356
|
var type = entry[0], count = entry[1];
|
|
9466
10357
|
return '<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">' +
|
|
9467
|
-
'<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi)">' + esc(type) + '</div>' +
|
|
10358
|
+
'<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi)" title="' + esc(type) + '">' + esc(type) + '</div>' +
|
|
9468
10359
|
'<div style="flex:1;height:7px;background:var(--border);border-radius:2px;overflow:hidden"><div style="width:' + Math.round(count/maxAg*100) + '%;height:100%;background:var(--accent);border-radius:2px"></div></div>' +
|
|
9469
10360
|
'<div style="width:22px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">' + count + '</div>' +
|
|
9470
10361
|
'</div>';
|
|
9471
10362
|
}).join('') : '') +
|
|
9472
|
-
(toolArr.length ? '<div class="m-group-title" style="margin-bottom:5px;margin-top:14px">Top Tools</div>' +
|
|
10363
|
+
(toolArr.length ? '<div class="m-group-title" style="margin-bottom:5px;margin-top:14px">Top Tools' + (toolAllArr.length > 15 ? '<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:6px">top 15 of ' + toolAllArr.length + '</span>' : '') + '</div>' +
|
|
9473
10364
|
toolArr.map(function(entry) {
|
|
9474
10365
|
var tool = entry[0], count = entry[1];
|
|
9475
10366
|
return '<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">' +
|
|
9476
|
-
'<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px">' + esc(tool) + '</div>' +
|
|
10367
|
+
'<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="' + esc(tool) + '">' + esc(tool) + '</div>' +
|
|
9477
10368
|
'<div style="flex:1;height:7px;background:var(--border);border-radius:2px;overflow:hidden"><div style="width:' + Math.round(count/maxTool*100) + '%;height:100%;background:oklch(62% 0.12 195);border-radius:2px"></div></div>' +
|
|
9478
10369
|
'<div style="width:22px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">' + count + '</div>' +
|
|
9479
10370
|
'</div>';
|
|
@@ -9537,7 +10428,7 @@ function mmSwitchTab(tab) {
|
|
|
9537
10428
|
const body = document.getElementById('mm-body');
|
|
9538
10429
|
if (!body) return;
|
|
9539
10430
|
if (tab === 'orgs') mmRenderOrgs(body);
|
|
9540
|
-
else if (tab === 'skills') mmRenderSkills(body);
|
|
10431
|
+
else if (tab === 'skills') { mmRenderSkills(body); setTimeout(() => document.getElementById('mm-skill-filter-input')?.focus(), 50); }
|
|
9541
10432
|
else if (tab === 'loops') mmRenderLoops(body);
|
|
9542
10433
|
else if (tab === 'createorg') mmRenderCreateOrg(body);
|
|
9543
10434
|
else if (tab === 'metrics') mmRenderMetrics(body);
|
|
@@ -9551,7 +10442,7 @@ function mmRenderOrgs(body) {
|
|
|
9551
10442
|
const running = o.running;
|
|
9552
10443
|
return `<div class="mm-skill-item" onclick="closeMastermind();v2SelectOrg(${JSON.stringify(o.name)});switchView('orgs')">
|
|
9553
10444
|
<span class="mm-skill-name">${esc(o.name)}</span>
|
|
9554
|
-
<span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${running ? '⬤ LIVE' : ''}</span>
|
|
10445
|
+
<span class="mm-skill-desc" title="${esc(o.goal || '')}">${esc((o.goal || '').slice(0, 60))}${(o.goal || '').length > 60 ? '…' : ''} ${running ? '⬤ LIVE' : ''}</span>
|
|
9555
10446
|
</div>`;
|
|
9556
10447
|
}).join('');
|
|
9557
10448
|
}
|
|
@@ -9560,8 +10451,8 @@ let _mmSkillFilter = '';
|
|
|
9560
10451
|
function mmRenderSkills(body) {
|
|
9561
10452
|
const q = _mmSkillFilter.toLowerCase();
|
|
9562
10453
|
const filtered = q ? _MM_SKILLS_CATALOG.filter(s => s.name.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : _MM_SKILLS_CATALOG;
|
|
9563
|
-
body.innerHTML = `<div class="filter-bar" style="margin-bottom:12px"><input class="filter-input" type="text" placeholder="Search skills…" value="${esc(_mmSkillFilter)}" oninput="_mmSkillFilter=this.value;mmRenderSkills(document.getElementById('mm-body'))"></div>` +
|
|
9564
|
-
filtered.map(s => `<div class="mm-skill-item" onclick="navigator.clipboard.writeText(${JSON.stringify(s.name)}).then(()=>showToast('Copied',${JSON.stringify(s.name)},'ok'))">
|
|
10454
|
+
body.innerHTML = `<div class="filter-bar" style="margin-bottom:12px"><input class="filter-input" id="mm-skill-filter-input" type="text" placeholder="Search skills…" value="${esc(_mmSkillFilter)}" oninput="_mmSkillFilter=this.value;mmRenderSkills(document.getElementById('mm-body'))"></div>` +
|
|
10455
|
+
filtered.map(s => `<div class="mm-skill-item" title="Click to copy: /${esc(s.name)}" onclick="navigator.clipboard.writeText(${JSON.stringify(s.name)}).then(()=>showToast('Copied',${JSON.stringify(s.name)},'ok'))">
|
|
9565
10456
|
<span class="mm-skill-name">${esc(s.name)}</span>
|
|
9566
10457
|
<span class="mm-skill-desc">${esc(s.desc)}</span>
|
|
9567
10458
|
</div>`).join('');
|
|
@@ -9573,19 +10464,66 @@ async function mmRenderLoops(body) {
|
|
|
9573
10464
|
const data = await apiFetch('/api/loops?dir=' + enc(DIR));
|
|
9574
10465
|
const loops = Array.isArray(data) ? data : (data.loops || []);
|
|
9575
10466
|
if (!loops.length) { body.innerHTML = '<div class="empty">No active loops. Use /mastermind:autodev --tillend to start one.</div>'; return; }
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
const
|
|
10467
|
+
const mmHasRepeat = loops.some(l => l.source === '_repeat.md');
|
|
10468
|
+
const mmRepeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
|
|
10469
|
+
const mmDeduped = loops.filter(l => {
|
|
10470
|
+
if (l.source === 'scheduled_tasks_lock' && mmHasRepeat) return false;
|
|
10471
|
+
if (l.source !== 'schedule_wakeup_hook') return true;
|
|
10472
|
+
const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
|
|
10473
|
+
return !m || !mmRepeatPrompts.has(m[1].trim());
|
|
10474
|
+
});
|
|
10475
|
+
const _mmLoopsRefresh = '<div style="display:flex;justify-content:flex-end;margin-bottom:8px"><button class="btn" title="Refresh loop status" style="font-size:10px" onclick="mmRenderLoops(document.getElementById(\'mm-body\'))">↺ Refresh</button></div>';
|
|
10476
|
+
body.innerHTML = _mmLoopsRefresh + mmDeduped.map(l => {
|
|
10477
|
+
const isHilMm = l.status === 'hil:pending';
|
|
10478
|
+
const isTillendMm = l.type === 'tillend';
|
|
10479
|
+
const curRep = l.currentRep || 0;
|
|
10480
|
+
const maxReps = l.maxReps || 0;
|
|
10481
|
+
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
|
|
10482
|
+
const isExplicitlyActiveMm = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
10483
|
+
const _STALE = 2 * 60 * 60 * 1000;
|
|
10484
|
+
const isOverdueMm = !l.status?.startsWith('hil') && !isExplicitlyActiveMm &&
|
|
10485
|
+
nextAt > 0 && nextAt <= Date.now();
|
|
10486
|
+
const isStaledActiveMm = isExplicitlyActiveMm && nextAt > 0 && (Date.now() - nextAt) > _STALE;
|
|
10487
|
+
const isFinishedMm = isOverdueMm || isStaledActiveMm ||
|
|
10488
|
+
(!isExplicitlyActiveMm && maxReps > 0 && curRep >= maxReps) ||
|
|
10489
|
+
l.status === 'finished' || l.status === 'done' ||
|
|
10490
|
+
l.status === 'complete' || l.status === 'completed' || l.status === 'expired';
|
|
10491
|
+
const running = !isFinishedMm && l.status !== 'stopped' && l.status !== 'paused';
|
|
10492
|
+
const _mmLp = (function(_l) {
|
|
10493
|
+
if (_l.command) return { userPrompt: _l.prompt || '', command: _l.command };
|
|
10494
|
+
const full = _l.prompt || '';
|
|
10495
|
+
const cmdM = full.match(/^(\/\S+)/);
|
|
10496
|
+
if (!cmdM) return { userPrompt: full, command: '' };
|
|
10497
|
+
const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
|
|
10498
|
+
let ti = 0;
|
|
10499
|
+
while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
|
|
10500
|
+
ti++;
|
|
10501
|
+
if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) ti++;
|
|
10502
|
+
}
|
|
10503
|
+
return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1] };
|
|
10504
|
+
})(l);
|
|
10505
|
+
const name = (l.name || _mmLp.userPrompt || _mmLp.command || 'loop').slice(0, 60);
|
|
10506
|
+
const ms = nextAt ? nextAt - Date.now() : 0;
|
|
10507
|
+
const cdown = ms > 0 ? (Math.floor(ms/60000) + 'm ' + Math.floor((ms%60000)/1000) + 's') : '';
|
|
10508
|
+
const intervalMm = fmtInterval(l.interval || l.schedule);
|
|
10509
|
+
const runCount = isTillendMm
|
|
10510
|
+
? `run ${curRep} / ∞${maxReps ? ' (cap: ' + maxReps + ')' : ''}`
|
|
10511
|
+
: (maxReps > 0 ? `run ${curRep} / ${maxReps}` : (curRep ? `run ${curRep}` : ''));
|
|
10512
|
+
const statusLabel = isHilMm ? '⚠ HIL' : (running ? 'active' : (isFinishedMm ? 'done' : 'stopped'));
|
|
10513
|
+
const statusColor = isHilMm ? 'oklch(75% 0.16 60)' : '';
|
|
10514
|
+
const typeIco = isTillendMm ? '∞ ' : '↺ ';
|
|
10515
|
+
const _sMs = l.startedAt ? (typeof l.startedAt === 'number' ? l.startedAt : new Date(l.startedAt).getTime()) : 0;
|
|
10516
|
+
const ageMs = _sMs > 0 && _sMs < Date.now() ? Date.now() - _sMs : 0;
|
|
10517
|
+
const ageStr = ageMs > 0 ? fmtDur(ageMs) : '';
|
|
10518
|
+
const metaParts = [intervalMm, ageStr ? 'running ' + ageStr : '', cdown ? 'next in ' + cdown : '', runCount].filter(Boolean).join(' · ');
|
|
9582
10519
|
return `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
|
|
9583
10520
|
<div style="display:flex;align-items:center;gap:8px">
|
|
9584
|
-
<span style="font-size:13px;color:var(--text-hi);flex:1">${esc(name)}</span>
|
|
9585
|
-
<span class="ss-pill ${running ? 'on' : ''}"
|
|
9586
|
-
${running ? `<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red)" onclick="stopLoop(event,${JSON.stringify(l.id||l.name||'')});mmSwitchTab('loops')">■ Stop</button>` : ''}
|
|
10521
|
+
<span style="font-size:13px;color:var(--text-hi);flex:1">${typeIco}${esc(name)}</span>
|
|
10522
|
+
<span class="ss-pill ${running && !isHilMm ? 'on' : ''}" style="${statusColor ? 'color:'+statusColor+';background:oklch(65% 0.15 60 / 0.15);border:none' : ''}">${statusLabel}</span>
|
|
10523
|
+
${running ? `<button class="btn" title="Stop this automation loop" style="font-size:10px;color:var(--red);border-color:var(--red)" onclick="stopLoop(event,${JSON.stringify(l.id||l.name||'')});mmSwitchTab('loops')">■ Stop</button>` : ''}
|
|
9587
10524
|
</div>
|
|
9588
|
-
<div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(
|
|
10525
|
+
<div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(metaParts)}</div>
|
|
10526
|
+
${isHilMm ? `<div style="font-size:10px;color:oklch(75% 0.16 60);margin-top:4px">⚠ Waiting for human response — check HIL file to resume</div>` : ''}
|
|
9589
10527
|
</div>`;
|
|
9590
10528
|
}).join('');
|
|
9591
10529
|
} catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
@@ -9599,17 +10537,17 @@ function mmRenderCreateOrg(body) {
|
|
|
9599
10537
|
<div><div class="le-lbl">Org Name</div><input id="mco-name" class="filter-input" placeholder="my-team"></div>
|
|
9600
10538
|
<div><div class="le-lbl">Goal</div><input id="mco-goal" class="filter-input" placeholder="Build and ship features autonomously"></div>
|
|
9601
10539
|
<div><div class="le-lbl">Topology</div>
|
|
9602
|
-
<select id="mco-topo" class="filter-input" style="cursor:pointer">
|
|
10540
|
+
<select id="mco-topo" class="filter-input" title="Agent coordination topology" style="cursor:pointer">
|
|
9603
10541
|
${topos.map(t => `<option>${t}</option>`).join('')}
|
|
9604
10542
|
</select>
|
|
9605
10543
|
</div>
|
|
9606
10544
|
<div><div class="le-lbl">Adapter</div>
|
|
9607
|
-
<select id="mco-adapter" class="filter-input" style="cursor:pointer">
|
|
10545
|
+
<select id="mco-adapter" class="filter-input" title="Agent communication adapter" style="cursor:pointer">
|
|
9608
10546
|
${adapters.map(a => `<option>${a}</option>`).join('')}
|
|
9609
10547
|
</select>
|
|
9610
10548
|
</div>
|
|
9611
|
-
<div><div class="le-lbl">Max Agents</div><input id="mco-agents" class="filter-input" type="number" value="8" min="1" max="50"></div>
|
|
9612
|
-
<button class="btn" style="width:fit-content;color:oklch(65% 0.16 295);border-color:oklch(65% 0.16 295)" onclick="mmGenerateOrgCmd()">Generate CLI Command</button>
|
|
10549
|
+
<div><div class="le-lbl">Max Agents</div><input id="mco-agents" class="filter-input" type="number" value="8" min="1" max="50" title="Maximum concurrent agents (1–50)"></div>
|
|
10550
|
+
<button class="btn" title="Generate a monomind CLI command to create this org" style="width:fit-content;color:oklch(65% 0.16 295);border-color:oklch(65% 0.16 295)" onclick="mmGenerateOrgCmd()">Generate CLI Command</button>
|
|
9613
10551
|
<div id="mco-cmd-out" style="display:none;font-family:var(--mono);font-size:12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px;word-break:break-all;cursor:pointer;color:var(--text-hi)" title="Click to copy" onclick="navigator.clipboard.writeText(this.textContent).then(()=>showToast('Copied','','ok'))"></div>
|
|
9614
10552
|
</div>`;
|
|
9615
10553
|
}
|
|
@@ -9653,6 +10591,7 @@ async function mmRenderMetrics(body) {
|
|
|
9653
10591
|
</div>
|
|
9654
10592
|
<div class="m-group-title" style="margin-bottom:8px">Swarm</div>
|
|
9655
10593
|
<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)"><span style="color:var(--text-hi)">Topology</span><span style="color:${swarmTopo !== 'IDLE' ? 'var(--green)' : 'var(--text-lo)'};font-family:var(--mono)">${esc(swarmTopo)}</span></div>
|
|
10594
|
+
<div style="margin-top:12px;display:flex;justify-content:flex-end"><button class="btn" title="Refresh metrics" style="font-size:10px" onclick="mmRenderMetrics(document.getElementById('mm-body'))">↺ Refresh</button></div>
|
|
9656
10595
|
`;
|
|
9657
10596
|
} catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
9658
10597
|
}
|
|
@@ -9678,13 +10617,13 @@ async function mmRenderGraph(body) {
|
|
|
9678
10617
|
<div class="chunk-stat"><div class="chunk-stat-val">${Object.keys(nodeTypes).length}</div><div class="chunk-stat-lbl">Types</div></div>
|
|
9679
10618
|
</div>
|
|
9680
10619
|
${topNodes.length ? `<div class="m-group-title" style="margin-bottom:6px">God Nodes</div>
|
|
9681
|
-
${topNodes.map(n => `<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)">
|
|
10620
|
+
${topNodes.map(n => `<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border);cursor:pointer" data-nid="${esc(n.id || n.name || '')}" title="Open '${esc(n.name || n.id || '')}' in Monograph wiki (degree: ${n.degree ?? '—'})" onclick="var _nid=this.dataset.nid;closeMastermind();switchView('monograph');setTimeout(function(){mgWikiJumpToNode(_nid)},300)" onmouseenter="this.style.background='var(--hover)'" onmouseleave="this.style.background=''">
|
|
9682
10621
|
<span style="color:var(--text-hi);font-family:var(--mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%">${esc(n.name || n.id || '—')}</span>
|
|
9683
|
-
<span style="color:var(--text-lo);font-family:var(--mono);font-size:11px">${n.degree ?? '—'}
|
|
10622
|
+
<span style="color:var(--text-lo);font-family:var(--mono);font-size:11px">${n.degree ?? '—'} ↗</span>
|
|
9684
10623
|
</div>`).join('')}` : ''}
|
|
9685
10624
|
<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
|
|
9686
|
-
<button class="btn" onclick="switchView('monograph');closeMastermind()">Open Monograph →</button>
|
|
9687
|
-
<button class="btn" onclick="fetch('/api/monograph-build?dir='+enc(DIR),{method:'POST'}).then(()=>showToast('Building','Graph rebuild started','ok'))">↺ Rebuild Graph</button>
|
|
10625
|
+
<button class="btn" title="Switch to the Monograph view" onclick="switchView('monograph');closeMastermind()">Open Monograph →</button>
|
|
10626
|
+
<button class="btn" title="Trigger a full knowledge graph rebuild" onclick="fetch('/api/monograph-build?dir='+enc(DIR),{method:'POST'}).then(()=>showToast('Building','Graph rebuild started','ok'))">↺ Rebuild Graph</button>
|
|
9688
10627
|
</div>
|
|
9689
10628
|
`;
|
|
9690
10629
|
} catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|