@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.
Files changed (146) hide show
  1. package/.claude/skills/mastermind/architect.md +4 -7
  2. package/.claude/skills/mastermind/autodev.md +1 -3
  3. package/.claude/skills/mastermind/idea.md +0 -8
  4. package/.claude/skills/mastermind/monitor.md +2 -2
  5. package/README.md +286 -129
  6. package/dist/src/commands/doctor.d.ts.map +1 -1
  7. package/dist/src/commands/doctor.js +55 -1
  8. package/dist/src/commands/doctor.js.map +1 -1
  9. package/dist/src/ui/collector.mjs +118 -9
  10. package/dist/src/ui/dashboard.html +1432 -493
  11. package/dist/src/ui/data/agent-avatars.html +763 -0
  12. package/dist/src/ui/data/agent-avatars.json +966 -0
  13. package/dist/src/ui/data/avatars/account-strategist.svg +58 -0
  14. package/dist/src/ui/data/avatars/accounts-payable.svg +54 -0
  15. package/dist/src/ui/data/avatars/adaptive-coordinator.svg +55 -0
  16. package/dist/src/ui/data/avatars/adaptive-coordinator2.svg +54 -0
  17. package/dist/src/ui/data/avatars/ai-citation.svg +57 -0
  18. package/dist/src/ui/data/avatars/ai-engineer.svg +61 -0
  19. package/dist/src/ui/data/avatars/analytics-reporter.svg +53 -0
  20. package/dist/src/ui/data/avatars/api-tester.svg +53 -0
  21. package/dist/src/ui/data/avatars/architecture.svg +54 -0
  22. package/dist/src/ui/data/avatars/automation-governance.svg +55 -0
  23. package/dist/src/ui/data/avatars/backend-dev.svg +53 -0
  24. package/dist/src/ui/data/avatars/benchmarker.svg +54 -0
  25. package/dist/src/ui/data/avatars/blockchain-auditor.svg +53 -0
  26. package/dist/src/ui/data/avatars/byzantine-coord.svg +57 -0
  27. package/dist/src/ui/data/avatars/case-analyst.svg +57 -0
  28. package/dist/src/ui/data/avatars/cicd-engineer.svg +55 -0
  29. package/dist/src/ui/data/avatars/cloud-architect.svg +54 -0
  30. package/dist/src/ui/data/avatars/code-review-swarm.svg +57 -0
  31. package/dist/src/ui/data/avatars/coder-v119.svg +57 -0
  32. package/dist/src/ui/data/avatars/coder.svg +58 -0
  33. package/dist/src/ui/data/avatars/collective-coord.svg +54 -0
  34. package/dist/src/ui/data/avatars/compliance-auditor.svg +58 -0
  35. package/dist/src/ui/data/avatars/consensus-coordinator.svg +54 -0
  36. package/dist/src/ui/data/avatars/content-creator.svg +54 -0
  37. package/dist/src/ui/data/avatars/crdt-synchronizer.svg +53 -0
  38. package/dist/src/ui/data/avatars/cro-specialist.svg +58 -0
  39. package/dist/src/ui/data/avatars/data-consolidator.svg +54 -0
  40. package/dist/src/ui/data/avatars/data-engineer.svg +53 -0
  41. package/dist/src/ui/data/avatars/database-optimizer.svg +61 -0
  42. package/dist/src/ui/data/avatars/deal-strategist.svg +54 -0
  43. package/dist/src/ui/data/avatars/defender.svg +53 -0
  44. package/dist/src/ui/data/avatars/devops-automator.svg +56 -0
  45. package/dist/src/ui/data/avatars/discovery-coach.svg +54 -0
  46. package/dist/src/ui/data/avatars/email-marketing.svg +57 -0
  47. package/dist/src/ui/data/avatars/embedded-firmware.svg +61 -0
  48. package/dist/src/ui/data/avatars/evidence-collector.svg +57 -0
  49. package/dist/src/ui/data/avatars/experiment-tracker.svg +53 -0
  50. package/dist/src/ui/data/avatars/feedback-synthesizer.svg +54 -0
  51. package/dist/src/ui/data/avatars/finance-tracker.svg +54 -0
  52. package/dist/src/ui/data/avatars/frontend-developer.svg +54 -0
  53. package/dist/src/ui/data/avatars/game-audio-engineer.svg +59 -0
  54. package/dist/src/ui/data/avatars/game-designer.svg +54 -0
  55. package/dist/src/ui/data/avatars/gossip-coordinator.svg +54 -0
  56. package/dist/src/ui/data/avatars/hierarchical-coord.svg +54 -0
  57. package/dist/src/ui/data/avatars/incident-commander.svg +57 -0
  58. package/dist/src/ui/data/avatars/infrastructure.svg +54 -0
  59. package/dist/src/ui/data/avatars/input-validator.svg +53 -0
  60. package/dist/src/ui/data/avatars/ios-developer.svg +54 -0
  61. package/dist/src/ui/data/avatars/issue-tracker.svg +53 -0
  62. package/dist/src/ui/data/avatars/judge.svg +55 -0
  63. package/dist/src/ui/data/avatars/launch-strategist.svg +54 -0
  64. package/dist/src/ui/data/avatars/legal-compliance.svg +53 -0
  65. package/dist/src/ui/data/avatars/level-designer.svg +53 -0
  66. package/dist/src/ui/data/avatars/load-balancer.svg +57 -0
  67. package/dist/src/ui/data/avatars/mcp-builder.svg +53 -0
  68. package/dist/src/ui/data/avatars/memory-coordinator.svg +55 -0
  69. package/dist/src/ui/data/avatars/mesh-coordinator.svg +55 -0
  70. package/dist/src/ui/data/avatars/ml-developer.svg +58 -0
  71. package/dist/src/ui/data/avatars/mobile-app-builder.svg +53 -0
  72. package/dist/src/ui/data/avatars/mobile-dev.svg +54 -0
  73. package/dist/src/ui/data/avatars/model-qa.svg +58 -0
  74. package/dist/src/ui/data/avatars/narrative-designer.svg +58 -0
  75. package/dist/src/ui/data/avatars/outbound-strategist.svg +55 -0
  76. package/dist/src/ui/data/avatars/path-validator.svg +54 -0
  77. package/dist/src/ui/data/avatars/payment-agent.svg +53 -0
  78. package/dist/src/ui/data/avatars/perf-analyzer.svg +58 -0
  79. package/dist/src/ui/data/avatars/pipeline-analyst.svg +54 -0
  80. package/dist/src/ui/data/avatars/planner.svg +55 -0
  81. package/dist/src/ui/data/avatars/pr-manager.svg +54 -0
  82. package/dist/src/ui/data/avatars/pricing-strategist.svg +54 -0
  83. package/dist/src/ui/data/avatars/product-manager.svg +54 -0
  84. package/dist/src/ui/data/avatars/production-validator.svg +54 -0
  85. package/dist/src/ui/data/avatars/project-shepherd.svg +54 -0
  86. package/dist/src/ui/data/avatars/proposal-strategist.svg +54 -0
  87. package/dist/src/ui/data/avatars/prosecutor.svg +57 -0
  88. package/dist/src/ui/data/avatars/pseudocode.svg +53 -0
  89. package/dist/src/ui/data/avatars/queen-coordinator.svg +55 -0
  90. package/dist/src/ui/data/avatars/quorum-manager.svg +53 -0
  91. package/dist/src/ui/data/avatars/raft-manager.svg +53 -0
  92. package/dist/src/ui/data/avatars/reality-checker.svg +58 -0
  93. package/dist/src/ui/data/avatars/recruitment.svg +58 -0
  94. package/dist/src/ui/data/avatars/refinement.svg +53 -0
  95. package/dist/src/ui/data/avatars/release-manager.svg +54 -0
  96. package/dist/src/ui/data/avatars/repo-architect.svg +54 -0
  97. package/dist/src/ui/data/avatars/researcher.svg +58 -0
  98. package/dist/src/ui/data/avatars/resource-allocator.svg +53 -0
  99. package/dist/src/ui/data/avatars/reviewer.svg +53 -0
  100. package/dist/src/ui/data/avatars/safe-executor.svg +53 -0
  101. package/dist/src/ui/data/avatars/sales-coach.svg +53 -0
  102. package/dist/src/ui/data/avatars/sales-engineer.svg +58 -0
  103. package/dist/src/ui/data/avatars/scout-explorer.svg +58 -0
  104. package/dist/src/ui/data/avatars/security-architect.svg +54 -0
  105. package/dist/src/ui/data/avatars/security-auditor.svg +55 -0
  106. package/dist/src/ui/data/avatars/senior-developer.svg +58 -0
  107. package/dist/src/ui/data/avatars/senior-pm.svg +58 -0
  108. package/dist/src/ui/data/avatars/seo-specialist.svg +57 -0
  109. package/dist/src/ui/data/avatars/social-media.svg +54 -0
  110. package/dist/src/ui/data/avatars/solidity-engineer.svg +58 -0
  111. package/dist/src/ui/data/avatars/sparc-coder.svg +58 -0
  112. package/dist/src/ui/data/avatars/sparc-coord.svg +56 -0
  113. package/dist/src/ui/data/avatars/specification.svg +57 -0
  114. package/dist/src/ui/data/avatars/sprint-prioritizer.svg +53 -0
  115. package/dist/src/ui/data/avatars/sre.svg +54 -0
  116. package/dist/src/ui/data/avatars/studio-operations.svg +53 -0
  117. package/dist/src/ui/data/avatars/studio-producer.svg +55 -0
  118. package/dist/src/ui/data/avatars/support-responder.svg +56 -0
  119. package/dist/src/ui/data/avatars/system-architect.svg +54 -0
  120. package/dist/src/ui/data/avatars/task-orchestrator.svg +56 -0
  121. package/dist/src/ui/data/avatars/technical-artist.svg +53 -0
  122. package/dist/src/ui/data/avatars/technical-writer.svg +59 -0
  123. package/dist/src/ui/data/avatars/tester.svg +53 -0
  124. package/dist/src/ui/data/avatars/threat-detection.svg +61 -0
  125. package/dist/src/ui/data/avatars/trend-researcher.svg +54 -0
  126. package/dist/src/ui/data/avatars/trial-director.svg +55 -0
  127. package/dist/src/ui/data/avatars/unity-architect.svg +54 -0
  128. package/dist/src/ui/data/avatars/visionos-engineer.svg +57 -0
  129. package/dist/src/ui/data/avatars/worker-specialist.svg +55 -0
  130. package/dist/src/ui/data/avatars/workflow-architect.svg +57 -0
  131. package/dist/src/ui/data/avatars/workflow-automation.svg +54 -0
  132. package/dist/src/ui/data/avatars/zk-steward.svg +54 -0
  133. package/dist/src/ui/server.mjs +75 -73
  134. package/dist/src/update/checker.d.ts.map +1 -1
  135. package/dist/src/update/checker.js +24 -7
  136. package/dist/src/update/checker.js.map +1 -1
  137. package/dist/src/update/index.d.ts.map +1 -1
  138. package/dist/src/update/index.js +3 -6
  139. package/dist/src/update/index.js.map +1 -1
  140. package/dist/tsconfig.tsbuildinfo +1 -1
  141. package/package.json +9 -9
  142. package/dist/src/ui/.monomind/data/pending-insights.jsonl +0 -0
  143. package/dist/src/ui/.monomind/data/ranked-context.json +0 -5
  144. package/dist/src/ui/.monomind/loops/mastermind-review-1778664132789.json +0 -16
  145. package/dist/src/ui/.monomind/sessions/current.json +0 -13
  146. 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 &nbsp;<kbd>↵</kbd> detail &nbsp;<kbd>/</kbd> find &nbsp;<kbd>G</kbd> live &nbsp;<kbd>A</kbd> ambient &nbsp;<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()">&#x232B; 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()">&#x232B; 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-stop-btn" onclick="v2StopOrg()" style="display:none;color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Stop</button>
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&#39;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
- document.getElementById('view-title').textContent = titles[v] || v;
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').textContent = DIR;
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').textContent = DIR;
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
- pollTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 30000);
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').textContent = 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').textContent = sess.id.slice(0, 8) + '…';
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 || new Date(ev.ts).getTime() >= cutoff);
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, '&#39;');
2587
+ const itemsData = JSON.stringify(g.items).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
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
- let lbl = '', detail = '', id = ev.id || ev.uuid || '';
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
- lbl = esc(t.length > 90 ? t.slice(0, 90) + '…' : t);
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, '&#39;');
2624
+ const evData = JSON.stringify(ev).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
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(ev.label || ev.name)}</div></div>
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, '&quot;')}).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).toLocaleTimeString() : '—'}</div></div>
2568
- <div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val mono">${esc((ev.id || '').slice(0, 24))}</div></div>
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, '&quot;')}).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).toLocaleTimeString() : '—'}</div></div>
2574
- <div class="d-row"><div class="d-lbl">Message</div><div class="d-val" style="white-space:pre-wrap">${esc(ev.text || '')}</div></div>
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, '&quot;')}).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
- // offset so first cell starts on Monday of the week 12 weeks ago
2603
- const todayDow = new Date().getDay(); // 0=Sun
2604
- // pad start so column 0 begins on Monday
2605
- const startOffset = todayDow === 0 ? 6 : todayDow - 1;
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
- document.getElementById('sctx-label').textContent = sess.lastPrompt || sess.id.slice(0, 16) + '…';
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
- document.getElementById('bdg-loops').textContent = loops.length || '';
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 name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 36);
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(l.interval || l.schedule || 'running')}</div>
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
- document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}`;
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 ?? '—'}</td>
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 ?? '—'}</td>` +
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
- pane.innerHTML = '<div class="m-group-title" style="margin-bottom:6px">Routing Feedback</div>' +
3008
- rows.slice(-40).reverse().map(r => {
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 maxVal = Math.max(...items.slice(0, maxItems).map(x => Number(x[valKey] || 0)), 0.0001);
3061
- return items.slice(0, maxItems).map(item => {
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 label = esc(String(item[labelKey] || '—').slice(0, 24));
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??'—'}</td>
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>`).join('');
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
- el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
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, '&#39;');
3516
+ const sData = JSON.stringify(s).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
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 projects = (data?.projects || []).slice(0, 8);
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 = `Last activity across ${projects.length} projects`;
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
- if (!cost) return;
3604
- if (budget.daily) {
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
- } else {
3614
- alertState.budgetAlert = null;
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 sorted = [...allSessions]
4336
+ const all = [...allSessions]
3865
4337
  .filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
3866
- .sort((a, b) => b.totalCost - a.totalCost)
3867
- .slice(0, 15);
4338
+ .sort((a, b) => b.totalCost - a.totalCost);
4339
+ const sorted = all.slice(0, 15);
3868
4340
  const body = document.getElementById('lb-body');
3869
- 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>'; return; }
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 - new Date(e.ts).getTime() < 300000).length;
4082
- const t15 = tools.filter(e => now - new Date(e.ts).getTime() < 900000).length;
4083
- const t60 = tools.filter(e => now - new Date(e.ts).getTime() < 3600000).length;
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 aTs = typeof (a.firstTs || a.mtime) === 'number' ? (a.firstTs || a.mtime) : new Date(a.firstTs || a.mtime).getTime();
4131
- const bTs = typeof (b.firstTs || b.mtime) === 'number' ? (b.firstTs || b.mtime) : new Date(b.firstTs || b.mtime).getTime();
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 prevTs = typeof (prev.firstTs || prev.mtime) === 'number' ? (prev.firstTs || prev.mtime) : new Date(prev.firstTs || prev.mtime).getTime();
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 currTs = typeof (curr.firstTs || curr.mtime) === 'number' ? (curr.firstTs || curr.mtime) : new Date(curr.firstTs || curr.mtime).getTime();
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
- document.getElementById('bdg-loops').textContent = loops.length || '';
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
- el.innerHTML = loops.map((l, idx) => {
4187
- const running = l.status !== 'stopped' && l.status !== 'paused';
4188
- const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
4189
- const interval = l.interval || l.schedule || '';
4190
- const fullPrompt = l.prompt || l.command || '';
4191
- const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
4192
- const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
4193
- const runs = l.currentRep != null ? l.currentRep : '—';
4194
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
4195
- const maxReps = l.maxReps || 0;
4196
- const curRep = l.currentRep || 0;
4197
- const pct = (maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
4198
- const progBar = (maxReps > 0 && running)
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 cdownSpan = (running && nextAt)
4202
- ? ` <span class="loop-cdown" data-nextat="${nextAt}">…</span>`
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
- return `<div class="loop-row" onclick="toggleLoop(this)">
4208
- <div class="loop-ico">↺</div>
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([interval, l.description].filter(Boolean).join(' · ').slice(0, 80))}${cdownSpan}</div>
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 ${running ? 'active' : 'stopped'}">${running ? 'active' : 'stopped'}</div>
4775
+ <div class="loop-status ${statusClass}">${statusLabel}</div>
4215
4776
  ${stopBtn}
4216
4777
  </div>
4217
4778
  <div class="loop-expand">
4218
- ${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
4219
- <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(interval || '')}</div></div>
4220
- <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${running ? ' running' : ' stopped'}</div></div>
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">Last run</div><div class="le-val">${esc(lastRun)}</div></div>
4223
- <div class="le-row"><div class="le-lbl">Run count</div><div class="le-val">${esc(String(runs))}</div></div>
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
- if (!confirm('Stop loop "' + id + '"?')) return;
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
- el.textContent = 'overdue';
4280
- el.classList.add('overdue');
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
- const m = Math.floor(ms / 60000), s = Math.floor((ms % 60000) / 1000);
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="${DAYS[d]} ${h}:00 — ${v} session${v!==1?'s':''}"></div>`;
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
- return `<div class="av2-row"><span class="av2-time">${esc(t)}</span><span class="av2-type">${esc(fmtOrgEvType(ev.type))}</span><span class="av2-msg">${esc(detail)}</span></div>`;
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</div>'; return; }
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</div>'; return; }
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 &lt;name&gt;</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</div>'; return; }
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</div>'; return; }
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, '&quot;')},'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, '&quot;')},'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</div>'; return; }
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
- agents.slice(0, 20).map(a => {
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</div>';
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 ts = new Date(e.ts || e.timestamp || e.created_at || 0).getTime();
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().slice(0, 20), runs };
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</div>'; return; }
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</div>'; return; }
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</div>'; return; }
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 &lt;name&gt;</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</div>'; return; }
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</div>'; return; }
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</div>'; return; }
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</div>'; return; }
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 { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'}); } catch(_) {}
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 times = stamped.map(ev => new Date(ev.ts).getTime());
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 = ((new Date(ev.ts).getTime() - tMin) / span * 100).toFixed(2);
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 lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`, ''];
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}\`: ${ev.label || ''}`);
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 &gt; to search all sessions</div>';
6207
- if (sq.length >= 2) searchSessions(sq);
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: 'Open Monograph', action: () => { const l = document.querySelector('.nav-item[data-view="monograph"]'); if (l) l.click(); } },
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: 'Open Loops', action: () => { const l = document.querySelector('.nav-item[data-view="loops"]'); if (l) l.click(); } },
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') switchView('memory');
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 sorted = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20);
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
- el.innerHTML = buckets.map(b => {
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 dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
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 dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
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 rows = data.tools.slice(0, 15).map((t, i) => {
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 rows = data.projects.slice(0, 10).map((p, i) => {
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 startTs = typeof (s.firstTs || s.mtime) === 'number' ? (s.firstTs || s.mtime) : new Date(s.firstTs || s.mtime).getTime();
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 in the DOM if it exists
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
- if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
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 diff = Date.now() - (typeof ts === 'number' ? ts : new Date(ts).getTime());
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
- return Math.floor(h / 24) + 'd';
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
- } catch (_) {}
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 ranked = nodes.map(n => {
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).slice(0, 20);
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 bcRanked = Object.entries(bc).sort((a,b) => b[1]-a[1]).slice(0,20);
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
- el.innerHTML = d.nodes.slice(0, 50).map(n => {
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
- return `<li>${esc(lbl)}</li>`;
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
- return '<div class="mem-item' + active + '" data-filename="' + esc(fname) + '" onclick="selectMem(this.dataset.filename)">' +
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
8988
- .replace(/\*\*(.+?)\*\*/g, (_, g) => '<strong>' + esc(g) + '</strong>')
8989
- .replace(/^#{1,3} (.+)/gm, (_, g) => '<div style="font-weight:600;color:var(--text-hi);margin:6px 0 2px">' + esc(g) + '</div>')
8990
- .replace(/^- (.+)/gm, (_, g) => '<div style="padding-left:12px">• ' + esc(g) + '</div>');
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) + ')">&#x270E; Edit</button>' +
8998
- '<button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">&#x2715; Delete</button>' +
9824
+ '<button class="btn" title="Edit this memory entry" onclick="openEditMemModal(' + JSON.stringify(filename) + ')">&#x270E; Edit</button>' +
9825
+ '<button class="btn" title="Delete this memory entry" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">&#x2715; 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 || r.created_at) + '</div>' +
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
- agents.slice(0, 15).map(a =>
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
- el.innerHTML = '<div class="m-group-title" style="margin-bottom:3px">Events</div>' +
9217
- events.slice(-40).map(e =>
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
- esc(relTime(e.ts || e.timestamp)) + ' <span style="color:var(--text-mid)">' + esc(e.type || e.kind || '?') + '</span> ' +
9220
- esc((e.message || e.data || '').toString().slice(0, 70)) +
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
- grid.innerHTML = list.slice(0, 200).map(c => {
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, '&quot;');
10121
+ const chunkContent = JSON.stringify(c.content || c.text || c.body || '').replace(/"/g, '&quot;');
10122
+ const chunkSrc = JSON.stringify(src).replace(/"/g, '&quot;');
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').forEach(el => {
9375
- el.style.display = (!lq || (el.dataset.search || '').includes(lq)) ? '' : 'none';
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 data = await apiFetch('/api/graph?dir=' + enc(DIR));
9404
- _agData = data;
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</div>'; return; }
9436
- el.innerHTML = sessions.map((s, i) =>
9437
- '<div class="sess-row" style="margin-bottom:4px" onclick="selectAgSession(' + i + ')">' +
9438
- '<div class="sr-top">' +
9439
- '<div class="sr-prompt" style="font-size:11px">' + esc((s.id || s.file || '').slice(-16)) + '</div>' +
9440
- '</div>' +
9441
- '<div style="font-size:10px;color:var(--text-lo);margin-top:1px">' + (s.spawnCount || 0) + ' spawns · ' + (s.toolCount || 0) + ' tools</div>' +
9442
- '</div>'
9443
- ).join('');
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 agArr = Object.entries(s.agentTypes || {}).sort((a, b) => b[1] - a[1]).slice(0, 12);
9453
- const toolArr = Object.entries(s.tools || {}).sort((a, b) => b[1] - a[1]).slice(0, 15);
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
- body.innerHTML = loops.map(l => {
9577
- const running = l.status !== 'stopped' && l.status !== 'paused';
9578
- const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
9579
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
9580
- const ms = nextAt ? nextAt - Date.now() : 0;
9581
- const cdown = ms > 0 ? (Math.floor(ms/60000) + 'm ' + Math.floor((ms%60000)/1000) + 's') : (running ? 'running' : 'stopped');
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' : ''}">${running ? 'active' : 'stopped'}</span>
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(l.interval||l.schedule||'')} ${cdown ? '· ' + cdown : ''} ${l.currentRep != null ? '· run ' + l.currentRep : ''}</div>
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 ?? '—'}</span>
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>'; }