@monoes/monomindcli 1.11.14 → 1.12.0

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 (427) hide show
  1. package/.claude/agents/generated/channel-intelligence-director.md +87 -0
  2. package/.claude/agents/generated/chief-growth-officer.md +88 -0
  3. package/.claude/agents/generated/content-seo-strategist.md +90 -0
  4. package/.claude/agents/generated/developer-community-strategist.md +91 -0
  5. package/.claude/agents/generated/outreach-partnership-strategist.md +90 -0
  6. package/.claude/agents/generated/social-media-strategist.md +91 -0
  7. package/.claude/agents/generated/video-visual-strategist.md +90 -0
  8. package/.claude/commands/mastermind/master.md +1 -1
  9. package/.claude/helpers/auto-memory-hook.mjs +13 -4
  10. package/.claude/helpers/control-start.cjs +5 -0
  11. package/.claude/helpers/event-logger.cjs +114 -0
  12. package/.claude/helpers/handlers/adr-draft-handler.cjs +19 -5
  13. package/.claude/helpers/handlers/agent-start-handler.cjs +13 -4
  14. package/.claude/helpers/handlers/compact-handler.cjs +2 -0
  15. package/.claude/helpers/handlers/edit-handler.cjs +1 -1
  16. package/.claude/helpers/handlers/gates-handler.cjs +3 -0
  17. package/.claude/helpers/handlers/graph-status-handler.cjs +14 -8
  18. package/.claude/helpers/handlers/loops-status-handler.cjs +5 -2
  19. package/.claude/helpers/handlers/route-handler.cjs +13 -6
  20. package/.claude/helpers/handlers/session-handler.cjs +11 -4
  21. package/.claude/helpers/handlers/session-restore-handler.cjs +21 -11
  22. package/.claude/helpers/handlers/task-handler.cjs +13 -5
  23. package/.claude/helpers/intelligence.cjs +7 -2
  24. package/.claude/helpers/loop-tracker.cjs +15 -3
  25. package/.claude/helpers/memory.cjs +6 -1
  26. package/.claude/helpers/router.cjs +5 -2
  27. package/.claude/helpers/session.cjs +2 -0
  28. package/.claude/helpers/statusline.cjs +10 -2
  29. package/.claude/helpers/utils/micro-agents.cjs +20 -4
  30. package/.claude/skills/mastermind/_protocol.md +25 -15
  31. package/.claude/skills/mastermind/architect.md +3 -3
  32. package/.claude/skills/mastermind/autodev.md +4 -2
  33. package/.claude/skills/mastermind/idea.md +10 -0
  34. package/.claude/skills/mastermind/ops.md +3 -3
  35. package/.claude/skills/mastermind/runorg.md +153 -86
  36. package/dist/src/agents/registry-builder.d.ts.map +1 -1
  37. package/dist/src/agents/registry-builder.js +2 -0
  38. package/dist/src/agents/registry-builder.js.map +1 -1
  39. package/dist/src/autopilot-state.d.ts.map +1 -1
  40. package/dist/src/autopilot-state.js +10 -5
  41. package/dist/src/autopilot-state.js.map +1 -1
  42. package/dist/src/benchmarks/benchmark-runner.d.ts.map +1 -1
  43. package/dist/src/benchmarks/benchmark-runner.js +13 -0
  44. package/dist/src/benchmarks/benchmark-runner.js.map +1 -1
  45. package/dist/src/benchmarks/metric-evaluators.d.ts.map +1 -1
  46. package/dist/src/benchmarks/metric-evaluators.js +20 -9
  47. package/dist/src/benchmarks/metric-evaluators.js.map +1 -1
  48. package/dist/src/browser/actions.d.ts.map +1 -1
  49. package/dist/src/browser/actions.js +10 -3
  50. package/dist/src/browser/actions.js.map +1 -1
  51. package/dist/src/browser/browser.d.ts.map +1 -1
  52. package/dist/src/browser/browser.js +12 -2
  53. package/dist/src/browser/browser.js.map +1 -1
  54. package/dist/src/browser/cdp.d.ts.map +1 -1
  55. package/dist/src/browser/cdp.js +21 -3
  56. package/dist/src/browser/cdp.js.map +1 -1
  57. package/dist/src/browser/har.d.ts.map +1 -1
  58. package/dist/src/browser/har.js +27 -5
  59. package/dist/src/browser/har.js.map +1 -1
  60. package/dist/src/commands/agent.d.ts.map +1 -1
  61. package/dist/src/commands/agent.js +11 -8
  62. package/dist/src/commands/agent.js.map +1 -1
  63. package/dist/src/commands/analyze.d.ts.map +1 -1
  64. package/dist/src/commands/analyze.js +36 -21
  65. package/dist/src/commands/analyze.js.map +1 -1
  66. package/dist/src/commands/autopilot.d.ts.map +1 -1
  67. package/dist/src/commands/autopilot.js +12 -4
  68. package/dist/src/commands/autopilot.js.map +1 -1
  69. package/dist/src/commands/benchmark.d.ts.map +1 -1
  70. package/dist/src/commands/benchmark.js +51 -8
  71. package/dist/src/commands/benchmark.js.map +1 -1
  72. package/dist/src/commands/browse.d.ts.map +1 -1
  73. package/dist/src/commands/browse.js +5 -2
  74. package/dist/src/commands/browse.js.map +1 -1
  75. package/dist/src/commands/claims.d.ts.map +1 -1
  76. package/dist/src/commands/claims.js +29 -11
  77. package/dist/src/commands/claims.js.map +1 -1
  78. package/dist/src/commands/cleanup.d.ts.map +1 -1
  79. package/dist/src/commands/cleanup.js +25 -5
  80. package/dist/src/commands/cleanup.js.map +1 -1
  81. package/dist/src/commands/config.d.ts.map +1 -1
  82. package/dist/src/commands/config.js +15 -7
  83. package/dist/src/commands/config.js.map +1 -1
  84. package/dist/src/commands/daemon.d.ts.map +1 -1
  85. package/dist/src/commands/daemon.js +6 -0
  86. package/dist/src/commands/daemon.js.map +1 -1
  87. package/dist/src/commands/deployment.d.ts.map +1 -1
  88. package/dist/src/commands/deployment.js +34 -19
  89. package/dist/src/commands/deployment.js.map +1 -1
  90. package/dist/src/commands/doctor.d.ts.map +1 -1
  91. package/dist/src/commands/doctor.js +38 -12
  92. package/dist/src/commands/doctor.js.map +1 -1
  93. package/dist/src/commands/guidance.d.ts.map +1 -1
  94. package/dist/src/commands/guidance.js +15 -2
  95. package/dist/src/commands/guidance.js.map +1 -1
  96. package/dist/src/commands/hive-mind.d.ts.map +1 -1
  97. package/dist/src/commands/hive-mind.js +37 -14
  98. package/dist/src/commands/hive-mind.js.map +1 -1
  99. package/dist/src/commands/hooks.d.ts.map +1 -1
  100. package/dist/src/commands/hooks.js +42 -25
  101. package/dist/src/commands/hooks.js.map +1 -1
  102. package/dist/src/commands/init.d.ts.map +1 -1
  103. package/dist/src/commands/init.js +9 -4
  104. package/dist/src/commands/init.js.map +1 -1
  105. package/dist/src/commands/issues.d.ts.map +1 -1
  106. package/dist/src/commands/issues.js +29 -26
  107. package/dist/src/commands/issues.js.map +1 -1
  108. package/dist/src/commands/mcp.d.ts.map +1 -1
  109. package/dist/src/commands/mcp.js +11 -5
  110. package/dist/src/commands/mcp.js.map +1 -1
  111. package/dist/src/commands/memory.d.ts.map +1 -1
  112. package/dist/src/commands/memory.js +10 -0
  113. package/dist/src/commands/memory.js.map +1 -1
  114. package/dist/src/commands/migrate.js +5 -5
  115. package/dist/src/commands/migrate.js.map +1 -1
  116. package/dist/src/commands/monograph.d.ts.map +1 -1
  117. package/dist/src/commands/monograph.js +18 -5
  118. package/dist/src/commands/monograph.js.map +1 -1
  119. package/dist/src/commands/monovector/backup.d.ts.map +1 -1
  120. package/dist/src/commands/monovector/backup.js +8 -2
  121. package/dist/src/commands/monovector/backup.js.map +1 -1
  122. package/dist/src/commands/monovector/benchmark.d.ts.map +1 -1
  123. package/dist/src/commands/monovector/benchmark.js +20 -7
  124. package/dist/src/commands/monovector/benchmark.js.map +1 -1
  125. package/dist/src/commands/monovector/import.d.ts.map +1 -1
  126. package/dist/src/commands/monovector/import.js +15 -0
  127. package/dist/src/commands/monovector/import.js.map +1 -1
  128. package/dist/src/commands/monovector/migrate.d.ts.map +1 -1
  129. package/dist/src/commands/monovector/migrate.js +4 -1
  130. package/dist/src/commands/monovector/migrate.js.map +1 -1
  131. package/dist/src/commands/monovector/optimize.d.ts.map +1 -1
  132. package/dist/src/commands/monovector/optimize.js +11 -0
  133. package/dist/src/commands/monovector/optimize.js.map +1 -1
  134. package/dist/src/commands/monovector/setup.d.ts.map +1 -1
  135. package/dist/src/commands/monovector/setup.js +11 -1
  136. package/dist/src/commands/monovector/setup.js.map +1 -1
  137. package/dist/src/commands/neural.js +1 -1
  138. package/dist/src/commands/neural.js.map +1 -1
  139. package/dist/src/commands/performance.d.ts.map +1 -1
  140. package/dist/src/commands/performance.js +20 -7
  141. package/dist/src/commands/performance.js.map +1 -1
  142. package/dist/src/commands/platforms.d.ts.map +1 -1
  143. package/dist/src/commands/platforms.js +90 -8
  144. package/dist/src/commands/platforms.js.map +1 -1
  145. package/dist/src/commands/plugins.d.ts.map +1 -1
  146. package/dist/src/commands/plugins.js +12 -5
  147. package/dist/src/commands/plugins.js.map +1 -1
  148. package/dist/src/commands/process.d.ts.map +1 -1
  149. package/dist/src/commands/process.js +33 -10
  150. package/dist/src/commands/process.js.map +1 -1
  151. package/dist/src/commands/progress.d.ts.map +1 -1
  152. package/dist/src/commands/progress.js +5 -3
  153. package/dist/src/commands/progress.js.map +1 -1
  154. package/dist/src/commands/providers.js +5 -5
  155. package/dist/src/commands/providers.js.map +1 -1
  156. package/dist/src/commands/replay.d.ts.map +1 -1
  157. package/dist/src/commands/replay.js +8 -2
  158. package/dist/src/commands/replay.js.map +1 -1
  159. package/dist/src/commands/route.d.ts.map +1 -1
  160. package/dist/src/commands/route.js +27 -7
  161. package/dist/src/commands/route.js.map +1 -1
  162. package/dist/src/commands/security.d.ts.map +1 -1
  163. package/dist/src/commands/security.js +4 -0
  164. package/dist/src/commands/security.js.map +1 -1
  165. package/dist/src/commands/session.d.ts.map +1 -1
  166. package/dist/src/commands/session.js +12 -1
  167. package/dist/src/commands/session.js.map +1 -1
  168. package/dist/src/commands/start.d.ts.map +1 -1
  169. package/dist/src/commands/start.js +11 -4
  170. package/dist/src/commands/start.js.map +1 -1
  171. package/dist/src/commands/status.d.ts.map +1 -1
  172. package/dist/src/commands/status.js +7 -4
  173. package/dist/src/commands/status.js.map +1 -1
  174. package/dist/src/commands/swarm.d.ts.map +1 -1
  175. package/dist/src/commands/swarm.js +27 -13
  176. package/dist/src/commands/swarm.js.map +1 -1
  177. package/dist/src/commands/task.d.ts.map +1 -1
  178. package/dist/src/commands/task.js +26 -11
  179. package/dist/src/commands/task.js.map +1 -1
  180. package/dist/src/commands/tokens.d.ts.map +1 -1
  181. package/dist/src/commands/tokens.js +7 -2
  182. package/dist/src/commands/tokens.js.map +1 -1
  183. package/dist/src/commands/transfer-store.d.ts.map +1 -1
  184. package/dist/src/commands/transfer-store.js +36 -22
  185. package/dist/src/commands/transfer-store.js.map +1 -1
  186. package/dist/src/commands/update.d.ts.map +1 -1
  187. package/dist/src/commands/update.js +15 -3
  188. package/dist/src/commands/update.js.map +1 -1
  189. package/dist/src/commands/workflow.d.ts.map +1 -1
  190. package/dist/src/commands/workflow.js +39 -6
  191. package/dist/src/commands/workflow.js.map +1 -1
  192. package/dist/src/consensus/audit-writer.d.ts.map +1 -1
  193. package/dist/src/consensus/audit-writer.js +18 -7
  194. package/dist/src/consensus/audit-writer.js.map +1 -1
  195. package/dist/src/consensus/vote-signer.d.ts.map +1 -1
  196. package/dist/src/consensus/vote-signer.js +25 -8
  197. package/dist/src/consensus/vote-signer.js.map +1 -1
  198. package/dist/src/index.d.ts.map +1 -1
  199. package/dist/src/index.js +7 -3
  200. package/dist/src/index.js.map +1 -1
  201. package/dist/src/init/executor.d.ts.map +1 -1
  202. package/dist/src/init/executor.js +14 -11
  203. package/dist/src/init/executor.js.map +1 -1
  204. package/dist/src/init/shared-instructions-generator.d.ts.map +1 -1
  205. package/dist/src/init/shared-instructions-generator.js +20 -4
  206. package/dist/src/init/shared-instructions-generator.js.map +1 -1
  207. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  208. package/dist/src/init/statusline-generator.js +36 -15
  209. package/dist/src/init/statusline-generator.js.map +1 -1
  210. package/dist/src/mcp-tools/a2a-tools.d.ts.map +1 -1
  211. package/dist/src/mcp-tools/a2a-tools.js +98 -13
  212. package/dist/src/mcp-tools/a2a-tools.js.map +1 -1
  213. package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
  214. package/dist/src/mcp-tools/agent-tools.js +16 -3
  215. package/dist/src/mcp-tools/agent-tools.js.map +1 -1
  216. package/dist/src/mcp-tools/analyze-tools.d.ts.map +1 -1
  217. package/dist/src/mcp-tools/analyze-tools.js +80 -17
  218. package/dist/src/mcp-tools/analyze-tools.js.map +1 -1
  219. package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
  220. package/dist/src/mcp-tools/browser-tools.js +84 -22
  221. package/dist/src/mcp-tools/browser-tools.js.map +1 -1
  222. package/dist/src/mcp-tools/claims-tools.d.ts.map +1 -1
  223. package/dist/src/mcp-tools/claims-tools.js +35 -7
  224. package/dist/src/mcp-tools/claims-tools.js.map +1 -1
  225. package/dist/src/mcp-tools/config-tools.d.ts.map +1 -1
  226. package/dist/src/mcp-tools/config-tools.js +82 -17
  227. package/dist/src/mcp-tools/config-tools.js.map +1 -1
  228. package/dist/src/mcp-tools/coordination-tools.d.ts.map +1 -1
  229. package/dist/src/mcp-tools/coordination-tools.js +37 -4
  230. package/dist/src/mcp-tools/coordination-tools.js.map +1 -1
  231. package/dist/src/mcp-tools/daa-tools.d.ts.map +1 -1
  232. package/dist/src/mcp-tools/daa-tools.js +49 -7
  233. package/dist/src/mcp-tools/daa-tools.js.map +1 -1
  234. package/dist/src/mcp-tools/embeddings-tools.d.ts.map +1 -1
  235. package/dist/src/mcp-tools/embeddings-tools.js +45 -18
  236. package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
  237. package/dist/src/mcp-tools/github-tools.d.ts.map +1 -1
  238. package/dist/src/mcp-tools/github-tools.js +75 -25
  239. package/dist/src/mcp-tools/github-tools.js.map +1 -1
  240. package/dist/src/mcp-tools/guidance-tools.d.ts.map +1 -1
  241. package/dist/src/mcp-tools/guidance-tools.js +32 -10
  242. package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
  243. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  244. package/dist/src/mcp-tools/hive-mind-tools.js +91 -20
  245. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  246. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  247. package/dist/src/mcp-tools/hooks-tools.js +188 -29
  248. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  249. package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
  250. package/dist/src/mcp-tools/memory-tools.js +25 -7
  251. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  252. package/dist/src/mcp-tools/monograph-compat.d.ts.map +1 -1
  253. package/dist/src/mcp-tools/monograph-compat.js +11 -2
  254. package/dist/src/mcp-tools/monograph-compat.js.map +1 -1
  255. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  256. package/dist/src/mcp-tools/monograph-tools.js +148 -26
  257. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  258. package/dist/src/mcp-tools/neural-tools.d.ts.map +1 -1
  259. package/dist/src/mcp-tools/neural-tools.js +44 -9
  260. package/dist/src/mcp-tools/neural-tools.js.map +1 -1
  261. package/dist/src/mcp-tools/performance-tools.d.ts.map +1 -1
  262. package/dist/src/mcp-tools/performance-tools.js +45 -10
  263. package/dist/src/mcp-tools/performance-tools.js.map +1 -1
  264. package/dist/src/mcp-tools/progress-tools.d.ts.map +1 -1
  265. package/dist/src/mcp-tools/progress-tools.js +7 -4
  266. package/dist/src/mcp-tools/progress-tools.js.map +1 -1
  267. package/dist/src/mcp-tools/request-tracker.d.ts.map +1 -1
  268. package/dist/src/mcp-tools/request-tracker.js +15 -1
  269. package/dist/src/mcp-tools/request-tracker.js.map +1 -1
  270. package/dist/src/mcp-tools/security-tools.d.ts.map +1 -1
  271. package/dist/src/mcp-tools/security-tools.js +61 -9
  272. package/dist/src/mcp-tools/security-tools.js.map +1 -1
  273. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  274. package/dist/src/mcp-tools/session-tools.js +45 -14
  275. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  276. package/dist/src/mcp-tools/swarm-tools.d.ts.map +1 -1
  277. package/dist/src/mcp-tools/swarm-tools.js +15 -3
  278. package/dist/src/mcp-tools/swarm-tools.js.map +1 -1
  279. package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
  280. package/dist/src/mcp-tools/system-tools.js +14 -7
  281. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  282. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  283. package/dist/src/mcp-tools/task-tools.js +52 -10
  284. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  285. package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
  286. package/dist/src/mcp-tools/terminal-tools.js +40 -6
  287. package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
  288. package/dist/src/mcp-tools/transfer-tools.d.ts.map +1 -1
  289. package/dist/src/mcp-tools/transfer-tools.js +37 -4
  290. package/dist/src/mcp-tools/transfer-tools.js.map +1 -1
  291. package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -1
  292. package/dist/src/mcp-tools/workflow-tools.js +29 -6
  293. package/dist/src/mcp-tools/workflow-tools.js.map +1 -1
  294. package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
  295. package/dist/src/memory/ewc-consolidation.js +26 -10
  296. package/dist/src/memory/ewc-consolidation.js.map +1 -1
  297. package/dist/src/memory/intelligence.d.ts.map +1 -1
  298. package/dist/src/memory/intelligence.js +80 -19
  299. package/dist/src/memory/intelligence.js.map +1 -1
  300. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  301. package/dist/src/memory/memory-bridge.js +21 -2
  302. package/dist/src/memory/memory-bridge.js.map +1 -1
  303. package/dist/src/memory/memory-initializer.d.ts.map +1 -1
  304. package/dist/src/memory/memory-initializer.js +67 -3
  305. package/dist/src/memory/memory-initializer.js.map +1 -1
  306. package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
  307. package/dist/src/memory/sona-optimizer.js +14 -4
  308. package/dist/src/memory/sona-optimizer.js.map +1 -1
  309. package/dist/src/monovector/command-outcomes.d.ts.map +1 -1
  310. package/dist/src/monovector/command-outcomes.js +43 -7
  311. package/dist/src/monovector/command-outcomes.js.map +1 -1
  312. package/dist/src/monovector/coverage-router.d.ts.map +1 -1
  313. package/dist/src/monovector/coverage-router.js +8 -4
  314. package/dist/src/monovector/coverage-router.js.map +1 -1
  315. package/dist/src/monovector/coverage-tools.d.ts.map +1 -1
  316. package/dist/src/monovector/coverage-tools.js +6 -3
  317. package/dist/src/monovector/coverage-tools.js.map +1 -1
  318. package/dist/src/monovector/diff-classifier.d.ts.map +1 -1
  319. package/dist/src/monovector/diff-classifier.js +13 -0
  320. package/dist/src/monovector/diff-classifier.js.map +1 -1
  321. package/dist/src/monovector/route-outcomes.d.ts +2 -1
  322. package/dist/src/monovector/route-outcomes.d.ts.map +1 -1
  323. package/dist/src/monovector/route-outcomes.js +46 -4
  324. package/dist/src/monovector/route-outcomes.js.map +1 -1
  325. package/dist/src/plugins/manager.d.ts.map +1 -1
  326. package/dist/src/plugins/manager.js +8 -3
  327. package/dist/src/plugins/manager.js.map +1 -1
  328. package/dist/src/plugins/store/discovery.d.ts.map +1 -1
  329. package/dist/src/plugins/store/discovery.js +46 -2
  330. package/dist/src/plugins/store/discovery.js.map +1 -1
  331. package/dist/src/plugins/store/search.d.ts.map +1 -1
  332. package/dist/src/plugins/store/search.js +5 -4
  333. package/dist/src/plugins/store/search.js.map +1 -1
  334. package/dist/src/production/circuit-breaker.d.ts.map +1 -1
  335. package/dist/src/production/circuit-breaker.js +17 -3
  336. package/dist/src/production/circuit-breaker.js.map +1 -1
  337. package/dist/src/production/error-handler.d.ts.map +1 -1
  338. package/dist/src/production/error-handler.js +3 -0
  339. package/dist/src/production/error-handler.js.map +1 -1
  340. package/dist/src/production/monitoring.d.ts.map +1 -1
  341. package/dist/src/production/monitoring.js +20 -3
  342. package/dist/src/production/monitoring.js.map +1 -1
  343. package/dist/src/production/rate-limiter.d.ts.map +1 -1
  344. package/dist/src/production/rate-limiter.js +13 -4
  345. package/dist/src/production/rate-limiter.js.map +1 -1
  346. package/dist/src/production/retry.d.ts.map +1 -1
  347. package/dist/src/production/retry.js +17 -9
  348. package/dist/src/production/retry.js.map +1 -1
  349. package/dist/src/routing/embed-worker.js +6 -2
  350. package/dist/src/routing/embed-worker.js.map +1 -1
  351. package/dist/src/routing/embedder.d.ts.map +1 -1
  352. package/dist/src/routing/embedder.js +0 -0
  353. package/dist/src/routing/embedder.js.map +1 -1
  354. package/dist/src/routing/llm-caller.d.ts.map +1 -1
  355. package/dist/src/routing/llm-caller.js +13 -2
  356. package/dist/src/routing/llm-caller.js.map +1 -1
  357. package/dist/src/routing/route-layer-factory.d.ts.map +1 -1
  358. package/dist/src/routing/route-layer-factory.js +18 -3
  359. package/dist/src/routing/route-layer-factory.js.map +1 -1
  360. package/dist/src/services/claim-service.d.ts +1 -0
  361. package/dist/src/services/claim-service.d.ts.map +1 -1
  362. package/dist/src/services/claim-service.js +8 -0
  363. package/dist/src/services/claim-service.js.map +1 -1
  364. package/dist/src/services/config-file-manager.d.ts.map +1 -1
  365. package/dist/src/services/config-file-manager.js +14 -2
  366. package/dist/src/services/config-file-manager.js.map +1 -1
  367. package/dist/src/services/headless-worker-executor.d.ts.map +1 -1
  368. package/dist/src/services/headless-worker-executor.js +18 -2
  369. package/dist/src/services/headless-worker-executor.js.map +1 -1
  370. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  371. package/dist/src/services/worker-daemon.js +53 -12
  372. package/dist/src/services/worker-daemon.js.map +1 -1
  373. package/dist/src/transfer/anonymization/index.d.ts +0 -3
  374. package/dist/src/transfer/anonymization/index.d.ts.map +1 -1
  375. package/dist/src/transfer/anonymization/index.js +16 -1
  376. package/dist/src/transfer/anonymization/index.js.map +1 -1
  377. package/dist/src/transfer/export.d.ts.map +1 -1
  378. package/dist/src/transfer/export.js +8 -0
  379. package/dist/src/transfer/export.js.map +1 -1
  380. package/dist/src/transfer/ipfs/upload.d.ts.map +1 -1
  381. package/dist/src/transfer/ipfs/upload.js +33 -3
  382. package/dist/src/transfer/ipfs/upload.js.map +1 -1
  383. package/dist/src/transfer/serialization/cfp.d.ts.map +1 -1
  384. package/dist/src/transfer/serialization/cfp.js +9 -3
  385. package/dist/src/transfer/serialization/cfp.js.map +1 -1
  386. package/dist/src/transfer/storage/gcs.d.ts.map +1 -1
  387. package/dist/src/transfer/storage/gcs.js +37 -3
  388. package/dist/src/transfer/storage/gcs.js.map +1 -1
  389. package/dist/src/transfer/store/discovery.d.ts.map +1 -1
  390. package/dist/src/transfer/store/discovery.js +45 -3
  391. package/dist/src/transfer/store/discovery.js.map +1 -1
  392. package/dist/src/transfer/store/download.d.ts.map +1 -1
  393. package/dist/src/transfer/store/download.js +5 -0
  394. package/dist/src/transfer/store/download.js.map +1 -1
  395. package/dist/src/transfer/store/publish.d.ts.map +1 -1
  396. package/dist/src/transfer/store/publish.js +13 -1
  397. package/dist/src/transfer/store/publish.js.map +1 -1
  398. package/dist/src/transfer/store/registry.d.ts +8 -0
  399. package/dist/src/transfer/store/registry.d.ts.map +1 -1
  400. package/dist/src/transfer/store/registry.js +30 -5
  401. package/dist/src/transfer/store/registry.js.map +1 -1
  402. package/dist/src/transfer/store/search.d.ts.map +1 -1
  403. package/dist/src/transfer/store/search.js +20 -5
  404. package/dist/src/transfer/store/search.js.map +1 -1
  405. package/dist/src/ui/collector.mjs +39 -5
  406. package/dist/src/ui/dashboard.html +934 -1282
  407. package/dist/src/ui/orgs.html +722 -12
  408. package/dist/src/ui/server.mjs +573 -134
  409. package/dist/src/update/checker.d.ts.map +1 -1
  410. package/dist/src/update/checker.js +59 -7
  411. package/dist/src/update/checker.js.map +1 -1
  412. package/dist/src/update/executor.d.ts.map +1 -1
  413. package/dist/src/update/executor.js +50 -3
  414. package/dist/src/update/executor.js.map +1 -1
  415. package/dist/src/update/index.d.ts.map +1 -1
  416. package/dist/src/update/index.js +18 -1
  417. package/dist/src/update/index.js.map +1 -1
  418. package/dist/src/update/rate-limiter.d.ts +6 -0
  419. package/dist/src/update/rate-limiter.d.ts.map +1 -1
  420. package/dist/src/update/rate-limiter.js +79 -7
  421. package/dist/src/update/rate-limiter.js.map +1 -1
  422. package/dist/src/update/validator.d.ts.map +1 -1
  423. package/dist/src/update/validator.js +52 -1
  424. package/dist/src/update/validator.js.map +1 -1
  425. package/dist/tsconfig.tsbuildinfo +1 -1
  426. package/package.json +2 -3
  427. package/dist/src/ui/data/mastermind-events.jsonl +0 -59
@@ -63,9 +63,10 @@ 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
+ .live-dot.polling { background: oklch(78% 0.16 80); animation-duration: 3s; }
67
67
  @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.35} }
68
68
  @media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }
69
+ .live-dot.polling { background: oklch(78% 0.16 80); animation-duration: 2s; }
69
70
  #tb-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
70
71
  .btn { font-size: 11px; color: var(--text-lo); background: transparent; border: 1px solid var(--border); border-radius: var(--r); padding: 4px 10px; cursor: pointer; transition: color 0.1s, border-color 0.1s; }
71
72
  .btn:hover { color: var(--text-hi); border-color: var(--text-lo); }
@@ -247,10 +248,13 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
247
248
  .lp-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.4s ease; }
248
249
  .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); }
249
250
  .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; }
251
+ .loop-status.done { background: var(--surface-hi); color: var(--text-xs); }
252
+ .loop-status.hil { background: oklch(78% 0.18 80 / 0.15); color: oklch(78% 0.18 80); }
253
+ .loop-row.hil { border-color: oklch(78% 0.18 80 / 0.3); }
254
+ .loop-hil-banner { margin-top: 6px; padding: 5px 8px; background: oklch(78% 0.18 80 / 0.1); border: 1px solid oklch(78% 0.18 80 / 0.3); border-radius: 4px; font-size: 11px; color: oklch(78% 0.18 80); }
255
+ .loop-type-badge { font-size: 15px; line-height: 1; color: var(--accent); }
256
+ .loop-type-badge.rep { color: var(--text-lo); }
257
+ .lp-bar { height: 4px; background: var(--border); border-radius: 2px; margin-top: 5px; overflow: hidden; position: relative; }
254
258
 
255
259
  /* memory */
256
260
  .mem-section { margin-bottom: 22px; }
@@ -599,7 +603,6 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
599
603
  .shm-grid { display:grid; grid-template-rows:repeat(7,10px); grid-auto-flow:column; grid-auto-columns:10px; gap:2px; margin-top:6px; }
600
604
  .shm-cell { border-radius:2px; background:var(--surface-hi); cursor:pointer; transition:outline 0.1s; }
601
605
  .shm-cell:hover { outline:1px solid var(--accent); outline-offset:-1px; }
602
- .shm-cell.shm-0 { cursor: default; }
603
606
  .shm-cell.shm-1 { background:oklch(72% 0.18 75 / 0.22); }
604
607
  .shm-cell.shm-2 { background:oklch(72% 0.18 75 / 0.42); }
605
608
  .shm-cell.shm-3 { background:oklch(72% 0.18 75 / 0.65); }
@@ -676,6 +679,38 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
676
679
  #rp-fill { height:100%; background:var(--accent); border-radius:2px; transition:width 0.15s; }
677
680
  #rp-counter { font-size:11px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; }
678
681
 
682
+ /* ── agent chat view ─────────────────────────────────────── */
683
+ #view-chat .vscroll { display:flex; flex-direction:column; height:100%; overflow:hidden; padding:0; }
684
+ #chat-v-bar { display:flex; align-items:center; gap:8px; padding:14px 18px 10px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
685
+ #chat-v-bar-title { font-size:13px; font-weight:600; color:var(--text-hi); }
686
+ #chat-v-sel { background:var(--surface); color:var(--text-mid); border:1px solid var(--border); border-radius:4px; font-size:11px; font-family:var(--mono); padding:3px 7px; cursor:pointer; max-width:300px; }
687
+ #chat-v-sel:focus { outline:none; border-color:var(--accent); }
688
+ #chat-v-live-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; margin-left:auto; transition:background 0.4s; }
689
+ #chat-v-live-dot.on { background:oklch(68% 0.20 150); animation:livepulse-cv 2.2s ease-in-out infinite; }
690
+ @keyframes livepulse-cv { 0%,100%{opacity:1} 50%{opacity:0.4} }
691
+ #chat-v-live-lbl { font-size:9px; color:var(--text-lo); }
692
+ #chat-v-feed { flex:1; overflow-y:auto; padding:12px 18px; display:flex; flex-direction:column; gap:5px; scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
693
+ #chat-v-feed::-webkit-scrollbar { width:4px; }
694
+ #chat-v-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
695
+ #chat-v-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
696
+ .cv-msg { display:flex; flex-direction:column; max-width:90%; }
697
+ .cv-msg.cv-sys { align-self:center; max-width:100%; }
698
+ .cv-msg.cv-agent { align-self:flex-start; }
699
+ .cv-msg.cv-ic { align-self:flex-start; }
700
+ @keyframes cv-in { from{opacity:0;transform:translateY(3px)} to{opacity:1;transform:none} }
701
+ .cv-msg.cv-new { animation:cv-in 0.18s ease-out; }
702
+ .cv-msg.cv-sys .cv-bub { display:flex; align-items:center; gap:6px; flex-wrap:wrap; background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:4px 12px; font-size:10px; color:var(--text-lo); text-align:center; }
703
+ .cv-msg.cv-agent .cv-bub { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; background:var(--surface-hi); border:1px solid oklch(62% 0.20 186 / 0.18); border-radius:2px 8px 8px 8px; padding:7px 11px; color:var(--text-mid); font-size:11px; line-height:1.6; word-break:break-word; white-space:pre-wrap; }
704
+ .cv-msg.cv-ic .cv-bub { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; background:oklch(10% 0.013 295); border:1px solid oklch(68% 0.18 295 / 0.22); border-radius:2px 8px 8px 8px; padding:7px 11px; color:oklch(68% 0.010 295); font-size:11px; line-height:1.6; word-break:break-word; white-space:pre-wrap; }
705
+ .cv-meta { display:flex; align-items:center; gap:5px; margin-bottom:3px; flex-wrap:wrap; }
706
+ .cv-msg.cv-sys .cv-meta { display:none; }
707
+ .cv-tag { font-size:8px; padding:1px 6px; border-radius:10px; border:1px solid oklch(62% 0.20 186 / 0.25); color:oklch(62% 0.20 186); letter-spacing:0.4px; flex-shrink:0; }
708
+ .cv-tag.cv-sender { border-color:oklch(68% 0.18 295 / 0.35); color:oklch(68% 0.14 295); }
709
+ .cv-tag.cv-receiver{ border-color:oklch(78% 0.18 80 / 0.35); color:oklch(78% 0.18 80); }
710
+ .cv-arrow { font-size:9px; color:var(--text-xs); }
711
+ .cv-ts { font-size:8px; color:var(--text-xs); margin-left:auto; }
712
+ .cv-etype { font-size:7px; padding:1px 4px; border-radius:2px; background:var(--surface); border:1px solid var(--border); color:var(--text-xs); letter-spacing:0.3px; }
713
+
679
714
  /* ── global feed (multi-project) ─────────────────────────── */
680
715
  .gf-proj-tag { font-size:10px; padding:1px 6px; border-radius:6px; background:var(--surface-hi); color:var(--text-lo); white-space:nowrap; flex-shrink:0; margin-top:3px; }
681
716
 
@@ -1235,8 +1270,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1235
1270
  .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; }
1236
1271
  .mg-query-row input:focus, .mg-query-row textarea:focus { border-color:var(--accent); }
1237
1272
  .mg-query-row textarea { resize:vertical; min-height:70px; width:100%; }
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; }
1273
+ .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; }
1240
1274
  /* live border glow */
1241
1275
  @keyframes live-fade { 0% { box-shadow: 0 0 0 1px oklch(72% 0.18 75 / 0.4); } 100% { box-shadow: none; } }
1242
1276
  .live-glow { animation: live-fade 8s ease-out forwards; }
@@ -1298,10 +1332,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1298
1332
  </div>
1299
1333
  <div id="sb-nav">
1300
1334
  <div class="nav-sect">
1301
- <div class="nav-item active" data-view="now" title="Now — current live activity feed">
1335
+ <div class="nav-item active" data-view="now">
1302
1336
  <span class="ico">◉</span><span class="lbl">Now</span>
1303
1337
  </div>
1304
- <div class="nav-item" data-view="projects" title="Projects — switch between projects">
1338
+ <div class="nav-item" data-view="projects">
1305
1339
  <span class="ico">⊞</span><span class="lbl">Projects</span>
1306
1340
  <span class="bdg" id="bdg-projects">—</span>
1307
1341
  </div>
@@ -1313,31 +1347,34 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1313
1347
  <div class="nav-proj-name" id="nav-proj-name">—</div>
1314
1348
  </div>
1315
1349
  <div class="nav-proj-items">
1316
- <div class="nav-item" data-view="sessions" title="Sessions — browse and replay project sessions">
1350
+ <div class="nav-item" data-view="sessions">
1317
1351
  <span class="ico">◫</span><span class="lbl">Sessions</span>
1318
1352
  <span class="bdg" id="bdg-sessions">—</span>
1319
1353
  </div>
1320
- <div class="nav-item" data-view="loops" title="Loops — manage automation loops">
1354
+ <div class="nav-item" data-view="loops">
1321
1355
  <span class="ico">↺</span><span class="lbl">Loops</span>
1322
1356
  <span class="bdg" id="bdg-loops">—</span>
1323
1357
  </div>
1324
- <div class="nav-item" data-view="tokens" title="Tokens — token usage and cost breakdown">
1358
+ <div class="nav-item" data-view="tokens">
1325
1359
  <span class="ico">$</span><span class="lbl">Tokens</span>
1326
1360
  </div>
1327
- <div class="nav-item" data-view="memory" title="Memory — agent memory store">
1361
+ <div class="nav-item" data-view="memory">
1328
1362
  <span class="ico">◈</span><span class="lbl">Memory</span>
1329
1363
  </div>
1330
- <div class="nav-item" data-view="orgs" title="Orgs — manage organizations and agents">
1364
+ <div class="nav-item" data-view="orgs">
1331
1365
  <span class="ico">⬡</span><span class="lbl">Orgs</span>
1332
1366
  </div>
1333
- <div class="nav-item" data-view="monograph" title="Monograph — knowledge graph explorer">
1367
+ <div class="nav-item" data-view="monograph">
1334
1368
  <span class="ico">⬡</span><span class="lbl">Monograph</span>
1335
1369
  </div>
1370
+ <div class="nav-item" data-view="chat">
1371
+ <span class="ico">⌘</span><span class="lbl">Agent Chat</span>
1372
+ </div>
1336
1373
  </div>
1337
1374
  </div>
1338
1375
  <div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
1339
1376
  <div class="nav-sect" style="margin-top:auto;padding-top:8px;">
1340
- <div class="nav-item" data-view="global" title="Global Feed — activity across all projects">
1377
+ <div class="nav-item" data-view="global">
1341
1378
  <span class="ico">⊕</span><span class="lbl">Global Feed</span>
1342
1379
  </div>
1343
1380
  <div class="nav-item" data-view="global-loops" title="Global Loops — loops across all projects">
@@ -1365,10 +1402,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1365
1402
  <span id="topbar-activity"></span>
1366
1403
  <div id="tb-right">
1367
1404
  <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>
1405
+ <button class="btn" onclick="switchView('orgs')" title="Manage autonomous organisations" style="color:oklch(70% 0.14 185);border-color:oklch(70% 0.14 185 / 0.4)">⬡ Orgs</button>
1406
+ <button class="btn" onclick="switchView('orgs')" title="Manage autonomous organisations" style="color:oklch(70% 0.14 185);border-color:oklch(70% 0.14 185 / 0.4)">⬡ Orgs</button>
1368
1407
  <button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</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>
1408
+ <button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
1370
1409
  <button class="btn" onclick="openShortcutHelp()" title="Keyboard shortcuts (?)">? Help</button>
1371
- <button class="btn" onclick="refreshCurrent()" title="Refresh current view">↺ Refresh</button>
1410
+ <button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
1372
1411
  </div>
1373
1412
  </div>
1374
1413
 
@@ -1414,13 +1453,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1414
1453
  <div id="feed-search">
1415
1454
  <input id="feed-search-input" type="text" placeholder="Search feed…" oninput="filterFeed(this.value)" onkeydown="if(event.key==='Escape')closeFeedSearch()">
1416
1455
  <span id="feed-search-count"></span>
1417
- <button id="feed-search-close" onclick="closeFeedSearch()" title="Close search">✕</button>
1456
+ <button id="feed-search-close" onclick="closeFeedSearch()">✕</button>
1418
1457
  </div>
1419
1458
  <div id="sess-ctx">
1420
- <button class="sctx-back" onclick="switchView('sessions')" title="Back to sessions list">← Sessions</button>
1459
+ <button class="sctx-back" onclick="switchView('sessions')">← Sessions</button>
1421
1460
  <span class="sctx-sep">/</span>
1422
1461
  <span class="sctx-label" id="sctx-label"></span>
1423
- <button class="sctx-live" onclick="goLive()" title="Switch to live feed mode">⬤ Go live</button>
1462
+ <button class="sctx-live" onclick="goLive()">⬤ Go live</button>
1424
1463
  </div>
1425
1464
  <div id="feed-recap"></div>
1426
1465
  <div id="replay-bar">
@@ -1434,10 +1473,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1434
1473
  <div id="feed-timeline" title="Session tool activity timeline"></div>
1435
1474
  <div id="feed-time-filter">
1436
1475
  <span class="tf-lbl">Range</span>
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>
1476
+ <button class="tf-btn active" data-tf="all" onclick="setFeedTimeFilter('all')">All</button>
1477
+ <button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
1478
+ <button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
1479
+ <button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
1441
1480
  <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>
1442
1481
  </div>
1443
1482
  <div id="weekly-card">
@@ -1466,7 +1505,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1466
1505
  <div id="detail-panel">
1467
1506
  <div id="detail-head">
1468
1507
  <h3 id="detail-title">Detail</h3>
1469
- <button id="detail-close" onclick="closeDetail()" title="Close detail panel">✕</button>
1508
+ <button id="detail-close" onclick="closeDetail()">✕</button>
1470
1509
  </div>
1471
1510
  <div id="detail-body"></div>
1472
1511
  </div>
@@ -1559,12 +1598,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1559
1598
  <table class="lb-table"><thead><tr>
1560
1599
  <th class="lb-rank">#</th><th>Session</th><th class="lb-cost">Cost</th><th class="lb-dur">Duration</th>
1561
1600
  </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>
1601
+ <div id="lb-overflow" style="font-size:10px;color:var(--text-xs);text-align:center;padding:6px 0"></div>
1563
1602
  </div>
1564
1603
  <div id="cost-histogram-panel"></div>
1565
1604
  <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>
1566
1605
  <div id="model-donut-panel"></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>
1606
+ <div id="file-pivot-bar"><span class="fpb-label" id="fpb-label"></span><button class="fpb-clear" onclick="clearFilePivot()">✕ Clear filter</button></div>
1568
1607
  <div id="model-mix-panel" style="display:none;margin-bottom:16px">
1569
1608
  <div id="model-mix-body"></div>
1570
1609
  </div>
@@ -1581,21 +1620,21 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1581
1620
  <div id="patterns-body"></div>
1582
1621
  </div>
1583
1622
  <div id="sess-heatmap" style="margin-bottom:14px;display:none">
1584
- <div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()" title="Clear date filter">✕ Clear filter</button></div>
1623
+ <div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()">✕ Clear filter</button></div>
1585
1624
  <div class="shm-grid" id="shm-grid"></div>
1586
1625
  </div>
1587
1626
  <div class="period-toggles" id="period-toggles">
1588
1627
  <span style="font-size:10px;color:var(--text-xs);align-self:center;text-transform:uppercase;letter-spacing:0.06em">Period:</span>
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>
1628
+ <button class="period-btn active" data-period="day" onclick="setPeriod('day')">Day</button>
1629
+ <button class="period-btn" data-period="week" onclick="setPeriod('week')">Week</button>
1630
+ <button class="period-btn" data-period="month" onclick="setPeriod('month')">Month</button>
1631
+ <button class="period-btn" data-period="all" onclick="setPeriod('all')">All</button>
1593
1632
  </div>
1594
1633
  <div id="bulk-toolbar">
1595
1634
  <span class="bulk-count" id="bulk-count">0 selected</span>
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>
1635
+ <button class="bulk-btn" onclick="bulkExport()">⬇ Export</button>
1636
+ <button class="bulk-btn" onclick="bulkBookmark()">☆ Bookmark all</button>
1637
+ <button class="bulk-btn danger" onclick="clearBulkSelection()">✕ Clear</button>
1599
1638
  </div>
1600
1639
  <div id="sess-filter-wrap">
1601
1640
  <input id="sess-filter-input" type="text" placeholder="Filter sessions by prompt…" oninput="filterSessions(this.value)" autocomplete="off">
@@ -1610,33 +1649,33 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1610
1649
  <div class="vscroll">
1611
1650
  <div class="pg-title">Loops</div>
1612
1651
  <div class="pg-sub">Scheduled automation loops</div>
1613
- <button id="btn-new-loop" onclick="showLoopForm()" title="Create a new automation loop">+ New Loop</button>
1652
+ <input id="loop-list-filter" class="filter-input" placeholder="Filter loops…" oninput="filterLoopList(this.value)" style="margin-bottom:10px;width:100%;max-width:380px">
1653
+ <button id="btn-new-loop" onclick="showLoopForm()">+ New Loop</button>
1614
1654
  <div id="loop-create-form" style="display:none">
1615
1655
  <div class="lcf-title">Create Loop</div>
1616
1656
  <div class="lcf-row">
1617
1657
  <label class="lcf-label">Prompt</label>
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>
1658
+ <textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?"></textarea>
1619
1659
  </div>
1620
1660
  <div class="lcf-row">
1621
1661
  <label class="lcf-label">Name (optional)</label>
1622
- <input class="lcf-input" id="lcf-name" type="text" placeholder="My loop" title="Optional display name for this loop">
1662
+ <input class="lcf-input" id="lcf-name" type="text" placeholder="My loop">
1623
1663
  </div>
1624
1664
  <div class="lcf-row-inline">
1625
1665
  <div class="lcf-row">
1626
1666
  <label class="lcf-label">Interval</label>
1627
- <input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h" title="How often to run (e.g. 30m, 1h, 2h)">
1667
+ <input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h">
1628
1668
  </div>
1629
1669
  <div class="lcf-row">
1630
1670
  <label class="lcf-label">Max reps (blank = ∞)</label>
1631
- <input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1" title="Maximum number of iterations before stopping (leave blank for unlimited)">
1671
+ <input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1">
1632
1672
  </div>
1633
1673
  </div>
1634
1674
  <div class="lcf-actions">
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>
1675
+ <button class="lcf-cancel" onclick="hideLoopForm()">Cancel</button>
1676
+ <button class="lcf-submit" onclick="createLoop()">Create Loop</button>
1637
1677
  </div>
1638
1678
  </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>
1640
1679
  <div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
1641
1680
  </div>
1642
1681
  </div>
@@ -1650,10 +1689,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1650
1689
  <div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:12px 14px">
1651
1690
  <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>
1652
1691
  <div class="tok-periods">
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>
1692
+ <button class="tok-period-btn active" data-period="today" onclick="setTokPeriod(this,'today')">Today</button>
1693
+ <button class="tok-period-btn" data-period="week" onclick="setTokPeriod(this,'week')">Week</button>
1694
+ <button class="tok-period-btn" data-period="30d" onclick="setTokPeriod(this,'30d')">30 Days</button>
1695
+ <button class="tok-period-btn" data-period="month" onclick="setTokPeriod(this,'month')">Month</button>
1657
1696
  </div>
1658
1697
  <canvas id="tok-chart" height="100" style="width:100%;display:block"></canvas>
1659
1698
  </div>
@@ -1667,18 +1706,15 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1667
1706
  <div class="pg-title">Memory</div>
1668
1707
  <div class="pg-sub">Knowledge palace — stored facts, graph, identity</div>
1669
1708
  <div class="mem-tab-bar" style="display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:6px">
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>
1709
+ <button class="odt-btn active" data-memtab="memories" onclick="switchMemTab('memories')">Memories</button>
1710
+ <button class="odt-btn" data-memtab="routing" onclick="switchMemTab('routing')">Routing</button>
1711
+ <button class="odt-btn" data-memtab="usage" onclick="switchMemTab('usage')">Usage</button>
1712
+ <button class="odt-btn" data-memtab="adrs" onclick="switchMemTab('adrs')">ADRs</button>
1713
+ <button class="odt-btn" data-memtab="swarm" onclick="switchMemTab('swarm')">Swarm</button>
1714
+ <button class="odt-btn" data-memtab="chunks" onclick="switchMemTab('chunks')">Chunks</button>
1715
+ <button class="odt-btn" data-memtab="agent-graph" onclick="switchMemTab('agent-graph')">Agent Graph</button>
1677
1716
  </div>
1678
1717
  <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>
1682
1718
  <div class="mem-split" id="mem-split">
1683
1719
  <div class="mem-list-pane" id="mem-list-pane">
1684
1720
  <div class="loading-txt" style="padding:16px">Loading…</div>
@@ -1687,7 +1723,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1687
1723
  <div style="color:var(--text-lo);font-size:13px;padding:20px 0">Select a memory</div>
1688
1724
  </div>
1689
1725
  </div>
1690
- <div style="margin-top:10px"><button class="btn" title="Create a new memory entry" onclick="openNewMemModal()">+ New Memory</button></div>
1726
+ <div style="margin-top:10px"><button class="btn" onclick="openNewMemModal()">+ New Memory</button></div>
1691
1727
  </div>
1692
1728
  <div id="mem-tab-routing" style="display:none"><div class="loading-txt">Loading…</div></div>
1693
1729
  <div id="mem-tab-usage" style="display:none"><div class="loading-txt">Loading…</div></div>
@@ -1704,7 +1740,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1704
1740
  <div style="color:var(--text-lo);font-size:13px">Select a swarm run</div>
1705
1741
  </div>
1706
1742
  </div>
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>
1743
+ <button class="btn" style="margin-top:10px;color:var(--red);border-color:var(--red)" onclick="cleanSwarmData()">&#x232B; Clean Data</button>
1708
1744
  </div>
1709
1745
  <div id="mem-tab-chunks" style="display:none">
1710
1746
  <div class="chunk-stats-bar" id="chunk-stats-bar">
@@ -1739,7 +1775,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1739
1775
  <div id="orgs-list-head">
1740
1776
  <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>
1741
1777
  <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>
1778
+ <input id="org-list-filter" class="filter-input" placeholder="Filter orgs…" oninput="filterOrgList(this.value)" style="width:100%;margin-top:8px">
1743
1779
  </div>
1744
1780
  <div id="orgs-list-scroll">
1745
1781
  <div class="loading-txt" id="orgs-list-content">Loading…</div>
@@ -1760,42 +1796,18 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1760
1796
  <span class="odh-pill" id="odh-topo">—</span>
1761
1797
  <span class="odh-pill" id="odh-roles">0 roles</span>
1762
1798
  <div class="odh-right">
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>
1765
- </div>
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>
1799
+ <button class="btn" id="org-copy-btn" onclick="v2ShowCopyOrgDialog()" style="color:var(--accent);border-color:var(--accent)">Copy to…</button>
1800
+ <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>
1788
1801
  </div>
1789
1802
  </div>
1790
-
1791
1803
  <div id="org-detail-tabs">
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>
1804
+ <button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
1805
+ <button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
1806
+ <button class="odt-btn" data-tab="health" onclick="v2SwitchOrgTab('health')">Health</button>
1807
+ <button class="odt-btn" data-tab="approvals" onclick="v2SwitchOrgTab('approvals')">Approvals</button>
1808
+ <button class="odt-btn" data-tab="budgets" onclick="v2SwitchOrgTab('budgets')">Budgets</button>
1809
+ <button class="odt-btn" data-tab="charts" onclick="v2SwitchOrgTab('charts')">Charts</button>
1810
+ <button class="odt-btn" data-tab="skills" onclick="v2SwitchOrgTab('skills')">Skills</button>
1799
1811
  </div>
1800
1812
  <div id="org-detail-body">
1801
1813
  <div class="odt-pane active" id="odt-chart"></div>
@@ -1829,7 +1841,27 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1829
1841
  <div class="odt-pane" id="odt-threads"></div>
1830
1842
  </div>
1831
1843
  </div>
1844
+ <div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.55);z-index:90;align-items:center;justify-content:center">
1845
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:320px;display:flex;flex-direction:column;gap:14px">
1846
+ <div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
1847
+ <div><input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%"></div>
1848
+ <div style="display:flex;gap:8px;justify-content:flex-end">
1849
+ <button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
1850
+ <button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
1851
+ </div>
1852
+ </div>
1853
+ </div>
1832
1854
  <!-- agent detail drawer (node-click / role-card-click) -->
1855
+ <div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.55);z-index:90;align-items:center;justify-content:center">
1856
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:320px;display:flex;flex-direction:column;gap:14px">
1857
+ <div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
1858
+ <input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%">
1859
+ <div style="display:flex;gap:8px;justify-content:flex-end">
1860
+ <button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
1861
+ <button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
1862
+ </div>
1863
+ </div>
1864
+ </div>
1833
1865
  <div id="org-agent-drawer" aria-hidden="true">
1834
1866
  <div id="oad-head"></div>
1835
1867
  <div id="oad-body"></div>
@@ -1844,13 +1876,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1844
1876
  <div class="pg-sub">Knowledge graph — dependencies, structure, analysis</div>
1845
1877
  <!-- Tab bar -->
1846
1878
  <div class="mg-tab-bar">
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>
1879
+ <button class="odt-btn active" data-mgtab="overview" onclick="mgSwitchTab('overview')">Overview</button>
1880
+ <button class="odt-btn" data-mgtab="graph" onclick="mgSwitchTab('graph')">Graph</button>
1881
+ <button class="odt-btn" data-mgtab="analyze" onclick="mgSwitchTab('analyze')">Analyze</button>
1882
+ <button class="odt-btn" data-mgtab="query" onclick="mgSwitchTab('query')">Query</button>
1883
+ <button class="odt-btn" data-mgtab="export" onclick="mgSwitchTab('export')">Export</button>
1884
+ <button class="odt-btn" data-mgtab="report" onclick="mgSwitchTab('report')">Report</button>
1885
+ <button class="odt-btn" data-mgtab="wiki" onclick="mgSwitchTab('wiki')">Wiki</button>
1854
1886
  </div>
1855
1887
 
1856
1888
  <!-- TAB 1: OVERVIEW -->
@@ -1876,9 +1908,9 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1876
1908
  <div class="mg-pane" id="mg-tab-graph">
1877
1909
  <iframe id="mg-iframe" src="" style="width:100%;height:500px;border:none;border-radius:6px;background:var(--surface);" title="Monograph graph"></iframe>
1878
1910
  <div class="mg-controls-row">
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>
1911
+ <button class="btn" id="mg-watch-btn" onclick="mgToggleWatch()">WATCH</button>
1912
+ <button class="btn" id="mg-rebuild-btn" onclick="mgRebuild()">REBUILD</button>
1913
+ <a class="btn" id="mg-open-tab-link" href="#" target="_blank" rel="noopener">OPEN IN TAB</a>
1882
1914
  <span class="mg-watch-indicator" id="mg-watch-status">○ IDLE</span>
1883
1915
  </div>
1884
1916
  </div>
@@ -1912,12 +1944,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1912
1944
  <div class="mqs-title">Impact analysis</div>
1913
1945
  <div class="mg-query-row">
1914
1946
  <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">
1947
+ <select id="mg-q-impact-dir" style="width:120px">
1916
1948
  <option value="both">Both</option>
1917
1949
  <option value="upstream">Upstream</option>
1918
1950
  <option value="downstream">Downstream</option>
1919
1951
  </select>
1920
- <button class="btn" title="Run impact analysis for this node" onclick="mgQueryImpact()">Run</button>
1952
+ <button class="btn" onclick="mgQueryImpact()">Run</button>
1921
1953
  </div>
1922
1954
  <div id="mg-q-impact-result" class="mg-query-result" style="display:none"></div>
1923
1955
  </div>
@@ -1925,7 +1957,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1925
1957
  <div class="mqs-title">Context</div>
1926
1958
  <div class="mg-query-row">
1927
1959
  <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>
1960
+ <button class="btn" onclick="mgQueryContext()">Run</button>
1929
1961
  </div>
1930
1962
  <div id="mg-q-ctx-result" class="mg-query-result" style="display:none"></div>
1931
1963
  </div>
@@ -1933,8 +1965,8 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1933
1965
  <div class="mqs-title">Shortest path</div>
1934
1966
  <div class="mg-query-row">
1935
1967
  <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>
1968
+ <input type="text" id="mg-q-path-to" placeholder="To node…" style="flex:1;min-width:120px">
1969
+ <button class="btn" onclick="mgQueryPath()">Run</button>
1938
1970
  </div>
1939
1971
  <div id="mg-q-path-result" class="mg-query-result" style="display:none"></div>
1940
1972
  </div>
@@ -1942,7 +1974,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1942
1974
  <div class="mqs-title">Cypher query</div>
1943
1975
  <div class="mg-query-row" style="flex-direction:column;align-items:stretch">
1944
1976
  <textarea id="mg-q-cypher" placeholder="MATCH (n) RETURN n.name LIMIT 20"></textarea>
1945
- <button class="btn" title="Execute Cypher query against the graph" style="align-self:flex-end;margin-top:6px" onclick="mgQueryCypher()">Run</button>
1977
+ <button class="btn" style="align-self:flex-end;margin-top:6px" onclick="mgQueryCypher()">Run</button>
1946
1978
  </div>
1947
1979
  <div id="mg-q-cypher-result" class="mg-query-result" style="display:none"></div>
1948
1980
  </div>
@@ -1950,31 +1982,31 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1950
1982
  <div class="mqs-title">Ask the graph</div>
1951
1983
  <div class="mg-query-row">
1952
1984
  <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">
1985
+ <select id="mg-q-ask-mode" style="width:110px">
1954
1986
  <option value="search">Search</option>
1955
1987
  <option value="explain">Explain</option>
1956
1988
  <option value="neighbors">Neighbors</option>
1957
1989
  </select>
1958
- <select id="mg-q-ask-budget" title="Max result count" style="width:80px">
1990
+ <select id="mg-q-ask-budget" style="width:80px">
1959
1991
  <option value="100">100</option>
1960
1992
  <option value="500">500</option>
1961
1993
  <option value="1000">1000</option>
1962
1994
  </select>
1963
- <button class="btn" title="Search or explain a node in the graph" onclick="mgQueryAsk()">Ask</button>
1995
+ <button class="btn" onclick="mgQueryAsk()">Ask</button>
1964
1996
  </div>
1965
1997
  <div id="mg-q-ask-result" class="mg-query-result" style="display:none"></div>
1966
1998
  </div>
1967
1999
  <div class="mg-query-section">
1968
2000
  <div class="mqs-title">Ripple impact <span style="font-size:10px;color:var(--text-xs);font-weight:400">multi-hop cascade</span></div>
1969
2001
  <div class="mg-query-row">
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()">
2002
+ <input type="text" id="mg-q-ripple-node" placeholder="Node name or file…" style="flex:1;min-width:160px">
1971
2003
  <select id="mg-q-ripple-hops" style="width:80px" title="Max hops">
1972
2004
  <option value="2">2 hops</option>
1973
2005
  <option value="3" selected>3 hops</option>
1974
2006
  <option value="4">4 hops</option>
1975
2007
  <option value="5">5 hops</option>
1976
2008
  </select>
1977
- <button class="btn" title="Trace multi-hop ripple impact from this node" onclick="mgQueryRipple()">Run</button>
2009
+ <button class="btn" onclick="mgQueryRipple()">Run</button>
1978
2010
  </div>
1979
2011
  <div id="mg-q-ripple-result" class="mg-query-result" style="display:none"></div>
1980
2012
  </div>
@@ -1983,7 +2015,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1983
2015
  <div class="mg-query-row">
1984
2016
  <input type="text" id="mg-q-chain-from" placeholder="From node…" style="flex:1;min-width:120px" onkeydown="if(event.key==='Enter')mgQueryImportChain()">
1985
2017
  <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>
2018
+ <button class="btn" onclick="mgQueryImportChain()">Run</button>
1987
2019
  </div>
1988
2020
  <div id="mg-q-chain-result" class="mg-query-result" style="display:none"></div>
1989
2021
  </div>
@@ -1999,7 +2031,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1999
2031
  <!-- TAB 6: REPORT -->
2000
2032
  <div class="mg-pane" id="mg-tab-report">
2001
2033
  <div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
2002
- <button class="btn" title="Reload the latest graph health report" onclick="mgLoadReport()">REFRESH REPORT</button>
2034
+ <button class="btn" onclick="mgLoadReport()">REFRESH REPORT</button>
2003
2035
  </div>
2004
2036
  <div id="mg-report-content"><div class="loading-txt">Loading…</div></div>
2005
2037
  </div>
@@ -2013,14 +2045,14 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2013
2045
  </div>
2014
2046
  <div class="mg-filter-pills" id="mg-wiki-pills"></div>
2015
2047
  <div class="mg-mode-toggle">
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>
2048
+ <button class="btn" id="mg-wiki-mode-all" onclick="mgWikiMode('all')" style="opacity:1">All</button>
2049
+ <button class="btn" id="mg-wiki-mode-docs" onclick="mgWikiMode('docs')" style="opacity:0.5">Docs</button>
2050
+ <button class="btn" id="mg-wiki-mode-code" onclick="mgWikiMode('code')" style="opacity:0.5">Code</button>
2019
2051
  </div>
2020
2052
  <div class="mg-query-row" style="margin-bottom:10px">
2021
2053
  <input type="text" id="mg-wiki-search" placeholder="Search nodes…" style="flex:1" oninput="mgWikiSearchDebounced(this.value)">
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>
2054
+ <button class="btn" onclick="mgRebuildDocs()" id="mg-build-docs-btn">BUILD DOCS</button>
2055
+ <button class="btn" onclick="mgWikiRefresh()">REFRESH</button>
2024
2056
  </div>
2025
2057
  <div id="mg-wiki-list"></div>
2026
2058
  <div id="mg-wiki-detail" class="mg-detail-panel" style="display:none"></div>
@@ -2039,24 +2071,43 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2039
2071
  </div>
2040
2072
  </div>
2041
2073
 
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>
2074
+ <!-- GLOBAL LOOPS -->
2075
+ <div class="view" id="view-global-loops">
2076
+ <div class="vscroll">
2077
+ <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
2078
+ <div class="pg-title" style="margin-bottom:0">Global Loops</div>
2079
+ <span class="pg-sub" id="gl-sub" style="margin-bottom:0">Loops across all projects</span>
2080
+ </div>
2081
+ <div id="gl-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
2047
2082
  </div>
2048
- <div id="gl-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
2049
2083
  </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>
2084
+
2085
+ <!-- GLOBAL TOKENS -->
2086
+ <div class="view" id="view-global-tokens">
2087
+ <div class="vscroll">
2088
+ <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
2089
+ <div class="pg-title" style="margin-bottom:0">Global Tokens</div>
2090
+ <span class="pg-sub" id="gt-sub" style="margin-bottom:0">Token usage across all projects</span>
2091
+ </div>
2092
+ <div id="gt-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
2093
+ </div>
2094
+ </div>
2095
+
2096
+ <div class="view" id="view-chat">
2097
+ <div class="vscroll">
2098
+ <div id="chat-v-bar">
2099
+ <span id="chat-v-bar-title">Agent Chat</span>
2100
+ <select id="chat-v-sel" onchange="chatVSelectSession(this.value)">
2101
+ <option value="">— select a session —</option>
2102
+ </select>
2103
+ <div id="chat-v-live-dot"></div>
2104
+ <span id="chat-v-live-lbl">OFFLINE</span>
2105
+ </div>
2106
+ <div id="chat-v-feed">
2107
+ <div id="chat-v-empty">Select a session above to see agent communications.<br>Agent messages, intercom signals, and system events appear here in real time.</div>
2108
+ </div>
2056
2109
  </div>
2057
- <div id="gt-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
2058
2110
  </div>
2059
- </div>
2060
2111
 
2061
2112
  </div><!-- /view-wrap -->
2062
2113
  </div><!-- /main -->
@@ -2070,12 +2121,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2070
2121
  <button id="mm-close" onclick="closeMastermind()" aria-label="Close">✕</button>
2071
2122
  </div>
2072
2123
  <div id="mm-tabs-bar">
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>
2124
+ <button class="mm-tab-btn active" data-mmtab="orgs" onclick="mmSwitchTab('orgs')">Orgs</button>
2125
+ <button class="mm-tab-btn" data-mmtab="skills" onclick="mmSwitchTab('skills')">Skills</button>
2126
+ <button class="mm-tab-btn" data-mmtab="loops" onclick="mmSwitchTab('loops')">Loops</button>
2127
+ <button class="mm-tab-btn" data-mmtab="createorg" onclick="mmSwitchTab('createorg')">Create Org</button>
2128
+ <button class="mm-tab-btn" data-mmtab="metrics" onclick="mmSwitchTab('metrics')">Metrics</button>
2129
+ <button class="mm-tab-btn" data-mmtab="graph" onclick="mmSwitchTab('graph')">Graph</button>
2079
2130
  </div>
2080
2131
  <div id="mm-body"></div>
2081
2132
  </div>
@@ -2085,10 +2136,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2085
2136
  <div id="chunk-modal-box">
2086
2137
  <div id="chunk-modal-title">Edit Chunk</div>
2087
2138
  <div id="chunk-modal-src"></div>
2088
- <textarea id="chunk-modal-ta" placeholder="Chunk content…" spellcheck="false"></textarea>
2139
+ <textarea id="chunk-modal-ta" spellcheck="false"></textarea>
2089
2140
  <div class="chunk-modal-btns">
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>
2141
+ <button class="btn" onclick="closeChunkModal()">Cancel</button>
2142
+ <button class="btn" style="color:var(--accent);border-color:var(--accent)" onclick="saveChunkModal()">Save</button>
2092
2143
  </div>
2093
2144
  </div>
2094
2145
  </div>
@@ -2096,10 +2147,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2096
2147
  <div id="mem-modal" onclick="if(event.target===this)closeMemModal()">
2097
2148
  <div id="mem-modal-box">
2098
2149
  <div id="mem-modal-title">Edit Memory</div>
2099
- <textarea id="mem-modal-ta" placeholder="Memory content (YAML or markdown)…" spellcheck="false"></textarea>
2150
+ <textarea id="mem-modal-ta" spellcheck="false"></textarea>
2100
2151
  <div class="mem-modal-btns">
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>
2152
+ <button class="btn" onclick="closeMemModal()">Cancel</button>
2153
+ <button class="btn" style="color:var(--accent);border-color:var(--accent)" onclick="saveMemModal()">Save</button>
2103
2154
  </div>
2104
2155
  </div>
2105
2156
  </div>
@@ -2110,8 +2161,8 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2110
2161
  <div id="report-box">
2111
2162
  <div class="rp-head">
2112
2163
  <span class="rp-title">Report Card</span>
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>
2164
+ <button class="rp-copy-btn" onclick="copyReportCard()">⎘ Copy</button>
2165
+ <button class="rp-close-btn" onclick="closeReportCard()">✕</button>
2115
2166
  </div>
2116
2167
  <div id="report-content"><pre id="report-pre"></pre></div>
2117
2168
  </div>
@@ -2120,7 +2171,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2120
2171
  <!-- shortcut help modal -->
2121
2172
  <div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
2122
2173
  <div id="shortcut-box">
2123
- <div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()" title="Close shortcuts help">✕</button></div>
2174
+ <div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
2124
2175
  <div class="sk-section">Sessions view</div>
2125
2176
  <div class="sk-row"><span class="sk-desc">Navigate rows</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
2126
2177
  <div class="sk-row"><span class="sk-desc">Open focused session</span><span class="sk-keys"><kbd>↵</kbd></span></div>
@@ -2129,17 +2180,12 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2129
2180
  <div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
2130
2181
  <div class="sk-row"><span class="sk-desc">Search in feed</span><span class="sk-keys"><kbd>/</kbd></span></div>
2131
2182
  <div class="sk-row"><span class="sk-desc">Jump to live session</span><span class="sk-keys"><kbd>G</kbd></span></div>
2183
+ <div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
2132
2184
  <div class="sk-row"><span class="sk-desc">Toggle ambient mode</span><span class="sk-keys"><kbd>A</kbd></span></div>
2133
2185
  <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>
2136
2186
  <div class="sk-row"><span class="sk-desc">Command palette</span><span class="sk-keys"><kbd>⌘</kbd><kbd>K</kbd></span></div>
2137
2187
  <div class="sk-row"><span class="sk-desc">Close / dismiss</span><span class="sk-keys"><kbd>Esc</kbd></span></div>
2138
2188
  <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>
2143
2189
  </div>
2144
2190
  </div>
2145
2191
 
@@ -2156,8 +2202,8 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2156
2202
  <input class="bm-input" id="bm-monthly" type="number" min="0" step="10" placeholder="e.g. 200">
2157
2203
  </div>
2158
2204
  <div class="bm-btns">
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>
2205
+ <button class="bm-cancel" onclick="closeBudgetModal()">Cancel</button>
2206
+ <button class="bm-save" onclick="saveBudget()">Save</button>
2161
2207
  </div>
2162
2208
  </div>
2163
2209
  </div>
@@ -2177,13 +2223,13 @@ let userScrolled = false;
2177
2223
  let selectedEntryId = null;
2178
2224
  let allDrawers = [];
2179
2225
  let dismissedAlerts = new Set();
2180
- let alertState = { todayCost: 0, monthCost: 0, errorCount: 0, longLoops: [], hilLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
2226
+ let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
2181
2227
  let feedTimeFilter = 'all';
2182
2228
  let cmdFocusIdx = 0;
2183
2229
  let cmdItems = [];
2184
2230
  let liveTailMode = false;
2185
2231
  let liveTailTimer = null;
2186
- let bookmarks = new Set((function(){ try { return JSON.parse(localStorage.getItem('mm-bookmarks') || '[]'); } catch { return []; } })());
2232
+ let bookmarks; try { bookmarks = new Set(JSON.parse(localStorage.getItem('mm-bookmarks') || '[]')); } catch { bookmarks = new Set(); }
2187
2233
  let showStarredOnly = false;
2188
2234
 
2189
2235
  // ── nav ────────────────────────────────────────────────────
@@ -2201,13 +2247,14 @@ function switchView(v) {
2201
2247
  el.classList.toggle('active', el.dataset.view === v));
2202
2248
  document.querySelectorAll('.view').forEach(el =>
2203
2249
  el.classList.toggle('active', el.id === 'view-' + 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`;
2250
+ 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', chat:'Agent Chat' };
2251
+ document.getElementById('view-title').textContent = titles[v] || v;
2252
+ const PROJECT = DIR ? shortPath(DIR) : 'monomind';
2253
+ const VIEW_LABELS = { now: 'Now', sessions: 'Sessions', projects: 'Projects', loops: 'Loops', tokens: 'Tokens', memory: 'Memory', orgs: 'Orgs', monograph: 'Monograph', global: 'Global Feed', 'global-loops': 'Global Loops', 'global-tokens': 'Global Tokens' };
2254
+ document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
2209
2255
  // Projects always re-fetches so onclick paths in cards stay current
2210
2256
  if (v === 'projects') { renderProjects(); return; }
2257
+ if (v === 'chat') { initChatView(); return; }
2211
2258
  if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
2212
2259
  }
2213
2260
 
@@ -2238,9 +2285,7 @@ async function init() {
2238
2285
  ORIGINAL_DIR = DIR;
2239
2286
  gitUser = gu;
2240
2287
  document.getElementById('sb-user').textContent = gu.name || gu.email || '—';
2241
- const sbPath = document.getElementById('sb-path');
2242
- sbPath.textContent = shortPath(DIR);
2243
- sbPath.title = DIR;
2288
+ document.getElementById('sb-path').textContent = DIR;
2244
2289
  document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
2245
2290
  _showNavProjectCtx(DIR);
2246
2291
  } catch (_) {}
@@ -2273,9 +2318,7 @@ async function init() {
2273
2318
  } catch (_) {}
2274
2319
  DIR = projParam;
2275
2320
  document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
2276
- const sbPath2 = document.getElementById('sb-path');
2277
- sbPath2.textContent = shortPath(DIR);
2278
- sbPath2.title = DIR;
2321
+ document.getElementById('sb-path').textContent = DIR;
2279
2322
  _showNavProjectCtx(DIR);
2280
2323
  }
2281
2324
  restoreURLParams();
@@ -2288,28 +2331,16 @@ async function init() {
2288
2331
 
2289
2332
  function _setLiveMode(mode) {
2290
2333
  const dot = document.querySelector('.live-dot');
2291
- const pill = dot?.closest('.pill');
2292
2334
  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
- }
2335
+ dot.classList.toggle('polling', mode === 'poll');
2336
+ const pill = dot.closest('.pill');
2337
+ if (pill) pill.childNodes.forEach(n => { if (n.nodeType === 3) n.textContent = mode === 'poll' ? ' polling' : ' live'; });
2302
2338
  }
2303
2339
 
2304
2340
  function startPolling() {
2305
- clearInterval(pollTimer);
2306
2341
  _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);
2342
+ clearInterval(pollTimer);
2343
+ pollTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 30000);
2313
2344
  }
2314
2345
 
2315
2346
  let _sseSource = null;
@@ -2319,18 +2350,24 @@ function initSSE() {
2319
2350
  try {
2320
2351
  const src = new EventSource('/api/events-stream?dir=' + enc(DIR));
2321
2352
  src.addEventListener('update', () => { if (currentView === 'now') refreshNowSilent(); });
2322
- src.addEventListener('connected', () => { _setLiveMode('sse'); });
2353
+ src.addEventListener('connected', () => {});
2323
2354
  src.onerror = () => { src.close(); _sseSource = null; startPolling(); };
2324
2355
  _sseSource = src;
2325
2356
  clearInterval(pollTimer); // SSE replaces polling
2326
- _setLiveMode('sse');
2327
2357
  } catch { startPolling(); }
2328
2358
  }
2329
2359
 
2330
- async function apiFetch(url) {
2331
- const r = await fetch(url);
2332
- if (!r.ok) throw new Error(`HTTP ${r.status}${r.statusText ? ' ' + r.statusText : ''}`);
2333
- return r.json();
2360
+ async function apiFetch(url, timeout = 15000) {
2361
+ const ctrl = new AbortController();
2362
+ const tid = setTimeout(() => ctrl.abort(), timeout);
2363
+ try {
2364
+ const r = await fetch(url, { signal: ctrl.signal });
2365
+ if (!r.ok) throw new Error(`HTTP ${r.status}${r.statusText ? ' ' + r.statusText : ''}`);
2366
+ return r.json();
2367
+ } catch (e) {
2368
+ if (e.name === 'AbortError') throw new Error('Request timed out');
2369
+ throw e;
2370
+ } finally { clearTimeout(tid); }
2334
2371
  }
2335
2372
 
2336
2373
  // ── project switching ──────────────────────────────────────
@@ -2362,9 +2399,7 @@ function switchProject(path) {
2362
2399
  _mgLoaded = false;
2363
2400
  _mgGraph = null;
2364
2401
  document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
2365
- const sbPath3 = document.getElementById('sb-path');
2366
- sbPath3.textContent = shortPath(path);
2367
- sbPath3.title = path;
2402
+ document.getElementById('sb-path').textContent = path;
2368
2403
  _showNavProjectCtx(path);
2369
2404
  viewRendered = {};
2370
2405
  allSessions = [];
@@ -2422,7 +2457,7 @@ async function loadFeed() {
2422
2457
  allSessions = sessions;
2423
2458
  document.getElementById('bdg-sessions').textContent = sessions.length || '—';
2424
2459
  if (!sessions.length) {
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>');
2460
+ setFeedContent('<div class="feed-empty">No sessions yet in this project.</div>');
2426
2461
  return;
2427
2462
  }
2428
2463
  sessionIdx = 0;
@@ -2453,9 +2488,7 @@ async function loadFeedSilent() {
2453
2488
 
2454
2489
  async function loadFeedForSession(sess) {
2455
2490
  if (!sess) return;
2456
- const feedSessEl = document.getElementById('feed-sess');
2457
- feedSessEl.textContent = sess.id.slice(0, 8) + '…';
2458
- feedSessEl.title = sess.id;
2491
+ document.getElementById('feed-sess').textContent = sess.id.slice(0, 8) + '…';
2459
2492
  document.getElementById('btn-prev-sess').style.opacity = sessionIdx < allSessions.length - 1 ? '1' : '0.3';
2460
2493
  document.getElementById('btn-next-sess').style.opacity = sessionIdx > 0 ? '1' : '0.3';
2461
2494
  showSessCtx(sess);
@@ -2513,7 +2546,7 @@ function catLabel(c) {
2513
2546
 
2514
2547
  function renderFeedEvents(events, silent) {
2515
2548
  if (!events.length) {
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>');
2549
+ if (!silent) setFeedContent('<div class="feed-empty">No events in this session yet.</div>');
2517
2550
  return;
2518
2551
  }
2519
2552
 
@@ -2530,7 +2563,7 @@ function renderFeedEvents(events, silent) {
2530
2563
  if (feedTimeFilter !== 'all') {
2531
2564
  const ms = { '1h': 3600000, '6h': 21600000, '24h': 86400000 }[feedTimeFilter] || 0;
2532
2565
  const cutoff = Date.now() - ms;
2533
- visible = filtered.filter(ev => !ev.ts || (typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || 0) >= cutoff);
2566
+ visible = filtered.filter(ev => !ev.ts || new Date(ev.ts).getTime() >= cutoff);
2534
2567
  }
2535
2568
 
2536
2569
  // update error alert state
@@ -2584,7 +2617,7 @@ function renderFeedEvents(events, silent) {
2584
2617
 
2585
2618
  function renderGroupRow(g) {
2586
2619
  const { ico, catCls } = toolStyle(g.cat, '');
2587
- const itemsData = JSON.stringify(g.items).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
2620
+ const itemsData = JSON.stringify(g.items).replace(/'/g, '&#39;');
2588
2621
  return `<div class="feed-group" data-items='${itemsData}' onclick="expandGroup(this)">
2589
2622
  <div class="feed-ico ${catCls}" style="font-size:9px">${ico}</div>
2590
2623
  <span class="fg-label">${g.count} ${esc(g.label)}</span>
@@ -2593,9 +2626,7 @@ function renderGroupRow(g) {
2593
2626
  }
2594
2627
 
2595
2628
  function expandGroup(el) {
2596
- let items;
2597
- try { items = JSON.parse(el.dataset.items); } catch { return; }
2598
- if (!Array.isArray(items)) return;
2629
+ const items = JSON.parse(el.dataset.items);
2599
2630
  const html = items.map(renderFeedEntry).join('');
2600
2631
  el.outerHTML = html;
2601
2632
  // re-apply active feed search to newly injected entries
@@ -2605,8 +2636,7 @@ function expandGroup(el) {
2605
2636
 
2606
2637
  function renderFeedEntry(ev) {
2607
2638
  const ts = ev.ts ? relTime(ev.ts) : '';
2608
- const tsTitle = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '';
2609
- let lbl = '', detail = '', lblTitle = '', id = ev.id || ev.uuid || '';
2639
+ let lbl = '', detail = '', id = ev.id || ev.uuid || '';
2610
2640
  let catCls, ico;
2611
2641
 
2612
2642
  if (ev.kind === 'tool') {
@@ -2616,21 +2646,20 @@ function renderFeedEntry(ev) {
2616
2646
  } else {
2617
2647
  ico = '↵'; catCls = 'cat-user';
2618
2648
  const t = (ev.text || '').trim();
2619
- if (t.length > 90) { lbl = esc(t.slice(0, 90) + '…'); lblTitle = esc(t); }
2620
- else lbl = esc(t);
2649
+ lbl = esc(t.length > 90 ? t.slice(0, 90) + '…' : t);
2621
2650
  }
2622
2651
 
2623
2652
  const errClass = ev._errored ? ' errored' : '';
2624
2653
  const selClass = selectedEntryId && selectedEntryId === id ? ' selected' : '';
2625
2654
 
2626
- const evData = JSON.stringify(ev).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
2655
+ const evData = JSON.stringify(ev).replace(/'/g, '&#39;');
2627
2656
  return `<div class="feed-entry k-${ev.kind}${errClass}${selClass}" data-ev='${evData}' onclick="openDetail(this.dataset.ev)">
2628
2657
  <div class="feed-ico ${catCls}">${ico}</div>
2629
2658
  <div class="feed-body">
2630
- <div class="feed-lbl"${lblTitle ? ` title="${lblTitle}"` : ''}>${lbl}</div>
2659
+ <div class="feed-lbl">${lbl}</div>
2631
2660
  ${detail ? `<div class="feed-detail">${detail}</div>` : ''}
2632
2661
  </div>
2633
- <div class="feed-ts"${tsTitle ? ` title="${tsTitle}"` : ''}>${ts}</div>
2662
+ <div class="feed-ts">${ts}</div>
2634
2663
  </div>`;
2635
2664
  }
2636
2665
 
@@ -2655,9 +2684,7 @@ function setFeedContent(html) {
2655
2684
 
2656
2685
  // ── detail panel ───────────────────────────────────────────
2657
2686
  function openDetail(evJson) {
2658
- let ev;
2659
- try { ev = JSON.parse(evJson); } catch { return; }
2660
- if (!ev) return;
2687
+ const ev = JSON.parse(evJson);
2661
2688
  selectedEntryId = ev.id || ev.uuid || '';
2662
2689
 
2663
2690
  const panel = document.getElementById('detail-panel');
@@ -2668,23 +2695,20 @@ function openDetail(evJson) {
2668
2695
 
2669
2696
  if (ev.kind === 'tool') {
2670
2697
  const { catCls } = toolStyle(ev.cat, ev.name);
2671
- const _toolId = (ev.id || '').toString();
2672
- const _toolLabel = ev.label || ev.name || '';
2673
2698
  title = ev.name || 'Tool';
2674
2699
  bodyHtml = `
2675
2700
  <div class="d-cat-pill ${catCls}" style="font-size:11px">${esc(ev.cat || 'other')}</div>
2676
- <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>
2701
+ <div class="d-row"><div class="d-lbl">Label</div><div class="d-val">${esc(ev.label || ev.name)}</div></div>
2677
2702
  ${ev.subagent ? `<div class="d-row"><div class="d-lbl">Subagent</div><div class="d-val">${esc(ev.subagent)}</div></div>` : ''}
2678
2703
  ${ev._errored ? `<div class="d-row"><div class="d-lbl">Status</div><div class="d-val error">Error</div></div>` : ''}
2679
- <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>
2680
- <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>
2704
+ <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
2705
+ <div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val mono">${esc((ev.id || '').slice(0, 24))}</div></div>
2681
2706
  `;
2682
2707
  } else if (ev.kind === 'user') {
2683
- const _userText = ev.text || '';
2684
2708
  title = 'User message';
2685
2709
  bodyHtml = `
2686
- <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>
2687
- <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>
2710
+ <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
2711
+ <div class="d-row"><div class="d-lbl">Message</div><div class="d-val" style="white-space:pre-wrap">${esc(ev.text || '')}</div></div>
2688
2712
  `;
2689
2713
  }
2690
2714
 
@@ -2712,10 +2736,10 @@ function buildSparkline() {
2712
2736
  if (idx >= 0 && idx < DAYS) buckets[idx]++;
2713
2737
  }
2714
2738
  const max = Math.max(...buckets, 1);
2715
- // pad so first column starts on Monday: compute day-of-week for day 0 (83 days ago)
2716
- const firstDow = new Date(now - (DAYS - 1) * DAY).getDay(); // 0=Sun
2717
- const startOffset = firstDow === 0 ? 6 : firstDow - 1; // Mon=0 offset
2718
- const padCells = Array.from({ length: startOffset }, () => '<div class="cal-cell" style="opacity:0"></div>');
2739
+ // offset so first cell starts on Monday of the week 12 weeks ago
2740
+ const todayDow = new Date().getDay(); // 0=Sun
2741
+ // pad start so column 0 begins on Monday
2742
+ const startOffset = todayDow === 0 ? 6 : todayDow - 1;
2719
2743
  const cells = buckets.map((v, i) => {
2720
2744
  const isToday = i === DAYS - 1;
2721
2745
  const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
@@ -2724,7 +2748,7 @@ function buildSparkline() {
2724
2748
  const title = `${label}: ${v} session${v !== 1 ? 's' : ''}`;
2725
2749
  return `<div class="cal-cell cal-${level}${isToday ? ' cal-today' : ''}" title="${title}"></div>`;
2726
2750
  });
2727
- return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${padCells.join('')}${cells.join('')}</div></div>`;
2751
+ return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${cells.join('')}</div></div>`;
2728
2752
  }
2729
2753
 
2730
2754
  // ── alerts rail ────────────────────────────────────────────
@@ -2754,10 +2778,6 @@ function updateAlerts() {
2754
2778
  all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
2755
2779
  }
2756
2780
 
2757
- for (const l of (alertState.hilLoops || [])) {
2758
- all.push({ id: 'hil-' + l, cls: 'alert-warn', ico: '⚠', msg: `Loop waiting for response: ${l}`, action: `switchView('loops')` });
2759
- }
2760
-
2761
2781
  const visible = all.filter(a => !dismissedAlerts.has(a.id));
2762
2782
  if (!visible.length) {
2763
2783
  rail.className = '';
@@ -2767,7 +2787,7 @@ function updateAlerts() {
2767
2787
  rail.className = 'has-alerts';
2768
2788
  rail.innerHTML = visible.map(a =>
2769
2789
  `<div class="alert-item ${a.cls}" data-alert-id="${a.id}"${a.action ? ` onclick="${a.action}" style="cursor:pointer"` : ''}>
2770
- <span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" title="Dismiss" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
2790
+ <span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
2771
2791
  </div>`).join('');
2772
2792
  }
2773
2793
 
@@ -2799,12 +2819,7 @@ function showSessCtx(sess) {
2799
2819
  bar.classList.remove('show');
2800
2820
  return;
2801
2821
  }
2802
- const sCtxAge = sess.lastTs || sess.mtime;
2803
- const sCtxTime = sCtxAge ? ' · ' + relTime(sCtxAge) : '';
2804
- const sCtxText = sess.lastPrompt || sess.id.slice(0, 16) + '…';
2805
- const label = document.getElementById('sctx-label');
2806
- label.textContent = sCtxText + sCtxTime;
2807
- label.title = sCtxText + (sCtxAge ? ' · ' + new Date(typeof sCtxAge === 'number' ? sCtxAge : Number(sCtxAge) || sCtxAge).toLocaleString() : '');
2822
+ document.getElementById('sctx-label').textContent = sess.lastPrompt || sess.id.slice(0, 16) + '…';
2808
2823
  bar.classList.add('show');
2809
2824
  }
2810
2825
 
@@ -2826,7 +2841,6 @@ async function loadTodayMetrics() {
2826
2841
  const data = await apiFetch('/api/section?name=tokens&dir=' + enc(DIR));
2827
2842
  const s = data?.tokens?.summary || {};
2828
2843
  alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
2829
- alertState.monthCost = typeof s.monthCost === 'number' ? s.monthCost : 0;
2830
2844
  updateAlerts();
2831
2845
  checkBudget();
2832
2846
  // topbar cost badge
@@ -2863,17 +2877,13 @@ async function loadLoopMetrics() {
2863
2877
  try {
2864
2878
  const data = await apiFetch('/api/loops?dir=' + enc(DIR));
2865
2879
  const loops = Array.isArray(data) ? data : (data.loops || []);
2866
- const hilCount = loops.filter(l => l.status === 'hil:pending').length;
2867
- document.getElementById('bdg-loops').textContent = loops.length ? (hilCount ? loops.length + '⚠' : loops.length) : '—';
2880
+ document.getElementById('bdg-loops').textContent = loops.length || '';
2868
2881
 
2869
2882
  // alert on loops running > 2h
2870
2883
  const TWO_HOURS = 2 * 3600 * 1000;
2871
2884
  const now = Date.now();
2872
2885
  alertState.longLoops = loops
2873
- .filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.status !== 'hil:pending' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
2874
- .map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
2875
- alertState.hilLoops = loops
2876
- .filter(l => l.status === 'hil:pending')
2886
+ .filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
2877
2887
  .map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
2878
2888
  updateAlerts();
2879
2889
 
@@ -2882,24 +2892,13 @@ async function loadLoopMetrics() {
2882
2892
  return;
2883
2893
  }
2884
2894
  const items = loops.slice(0, 5).map(l => {
2885
- const fullName = (l.name || l.prompt || 'loop').split('--')[0].trim();
2886
- const name = fullName.slice(0, 36);
2887
- const isHilMini = l.status === 'hil:pending';
2888
- const isTillendMini = l.type === 'tillend';
2889
- const intervalMini = fmtInterval(l.interval || l.schedule) || 'running';
2890
- const repMini = isTillendMini && l.currentRep ? `run ${l.currentRep}${l.maxReps ? '/' + l.maxReps : ''}` : null;
2891
- const hilDot = isHilMini ? ' <span style="color:oklch(75% 0.16 60);font-size:9px">⚠HIL</span>' : '';
2892
- const typeDot = isTillendMini ? '<span style="color:oklch(70% 0.18 280);font-size:9px;margin-right:3px">∞</span>' : '';
2895
+ const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 36);
2893
2896
  return `<div class="mini-loop">
2894
- <div class="ml-name" title="${esc(fullName)}">${typeDot}${esc(name)}${hilDot}</div>
2895
- <div class="ml-meta"><span class="ml-dot"></span>${esc(repMini || intervalMini)}</div>
2897
+ <div class="ml-name">${esc(name)}</div>
2898
+ <div class="ml-meta"><span class="ml-dot"></span>${esc(l.interval || l.schedule || 'running')}</div>
2896
2899
  </div>`;
2897
2900
  }).join('');
2898
- const overflow = loops.length > 5 ? loops.length - 5 : 0;
2899
- const overflowNote = overflow > 0
2900
- ? `<div style="font-size:10px;color:var(--text-xs);padding:3px 0 0">+${overflow} more — open Loops tab</div>`
2901
- : '';
2902
- document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}${overflowNote}`;
2901
+ document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}`;
2903
2902
  } catch (_) {
2904
2903
  document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt">—</div>`;
2905
2904
  }
@@ -2931,16 +2930,16 @@ async function loadStatusStrip() {
2931
2930
 
2932
2931
  // HNSW status
2933
2932
  const hnswOn = mem.hnsw === true || mem.hnswEnabled === true || mem.hnsw_enabled === true;
2934
- 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>`);
2933
+ pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
2935
2934
 
2936
2935
  // Patterns count
2937
2936
  if (mem.patterns != null) {
2938
- pills.push(`<span class="ss-pill" title="Learned routing patterns stored in AgentDB">PATTERNS ${Number(mem.patterns).toLocaleString()}</span>`);
2937
+ pills.push(`<span class="ss-pill">PATTERNS ${Number(mem.patterns).toLocaleString()}</span>`);
2939
2938
  }
2940
2939
 
2941
2940
  // Chunks count
2942
2941
  if (mem.chunks != null) {
2943
- pills.push(`<span class="ss-pill" title="Knowledge chunks indexed for semantic search">CHUNKS ${Number(mem.chunks).toLocaleString()}</span>`);
2942
+ pills.push(`<span class="ss-pill">CHUNKS ${Number(mem.chunks).toLocaleString()}</span>`);
2944
2943
  }
2945
2944
 
2946
2945
  // Swarm status
@@ -2972,7 +2971,7 @@ async function loadTokensView() {
2972
2971
  const rows = Array.isArray(data?.tokens?.rows) ? data.tokens.rows : [];
2973
2972
  cards.innerHTML = [
2974
2973
  { label:'Today Cost', val: typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—' },
2975
- { label:'Today Calls', val: s.todayCalls != null ? Number(s.todayCalls).toLocaleString() : '—' },
2974
+ { label:'Today Calls', val: s.todayCalls ?? '—' },
2976
2975
  { label:'Month Cost', val: typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—' },
2977
2976
  { label:'Total Tokens', val: s.totalTokens != null ? Number(s.totalTokens).toLocaleString() : '—' },
2978
2977
  ].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('');
@@ -2984,13 +2983,12 @@ async function loadTokensView() {
2984
2983
  '<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>' +
2985
2984
  '</tr></thead><tbody>' +
2986
2985
  rows.slice(0, 30).map(r => `<tr style="border-top:1px solid var(--border)">
2987
- <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>
2988
- <td style="padding:4px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>
2986
+ <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>
2987
+ <td style="padding:4px 8px;color:var(--text-lo)">${r.calls ?? '—'}</td>
2989
2988
  <td style="padding:4px 8px;color:var(--text-lo)">${r.tokens != null ? Number(r.tokens).toLocaleString() : '—'}</td>
2990
2989
  <td style="padding:4px 8px;color:var(--accent)">$${Number(r.cost ?? 0).toFixed(4)}</td>
2991
2990
  </tr>`).join('') +
2992
- '</tbody></table></div>' +
2993
- (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>` : '');
2991
+ '</tbody></table></div>';
2994
2992
  } else { table.innerHTML = ''; }
2995
2993
  markLiveGlow('view-tokens');
2996
2994
  } catch (_) {
@@ -3042,6 +3040,7 @@ function renderTokChart(daily, animated = true) {
3042
3040
  });
3043
3041
  canvas.addEventListener('mouseleave', () => { canvas._tokTip.style.display = 'none'; });
3044
3042
  }
3043
+
3045
3044
  const targets = vals.map((v, i) => ({
3046
3045
  v, i,
3047
3046
  isToday: i === vals.length - 1,
@@ -3102,7 +3101,7 @@ async function setTokPeriod(btn, period) {
3102
3101
  if (cards) cards.innerHTML = [
3103
3102
  { label: 'Cost', val: typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2)
3104
3103
  : typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '—' },
3105
- { label: 'Calls', val: (s.todayCalls ?? s.calls) != null ? Number(s.todayCalls ?? s.calls).toLocaleString() : '—' },
3104
+ { label: 'Calls', val: s.todayCalls ?? s.calls ?? '—' },
3106
3105
  { label: 'Tokens', val: s.totalTokens != null ? Number(s.totalTokens).toLocaleString() : '—' },
3107
3106
  { label: 'Models', val: s.modelCount ?? s.models ?? '—' },
3108
3107
  ].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('');
@@ -3114,13 +3113,12 @@ async function setTokPeriod(btn, period) {
3114
3113
  '<th style="padding:3px 8px">Calls</th><th style="padding:3px 8px">Cost</th></tr></thead><tbody>' +
3115
3114
  rows.slice(0, 30).map(r =>
3116
3115
  `<tr style="border-top:1px solid var(--border)">` +
3117
- `<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>` +
3118
- `<td style="padding:3px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>` +
3116
+ `<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>` +
3117
+ `<td style="padding:3px 8px;color:var(--text-lo)">${r.calls ?? '—'}</td>` +
3119
3118
  `<td style="padding:3px 8px;color:var(--accent)">$${Number(r.cost ?? 0).toFixed(4)}</td>` +
3120
3119
  `</tr>`
3121
3120
  ).join('') +
3122
- '</tbody></table></div>' +
3123
- (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>` : '');
3121
+ '</tbody></table></div>';
3124
3122
  } else if (table) { table.innerHTML = ''; }
3125
3123
  markLiveGlow('view-tokens');
3126
3124
  // Update topbar badge when showing today's data
@@ -3173,58 +3171,25 @@ async function loadMemRouting() {
3173
3171
  const last = rows[rows.length - 1];
3174
3172
  window._lastRouteAgent = last.suggestedAgent || last.route || last.category || last.agent || last.agentType || '';
3175
3173
  }
3176
- 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; }
3177
- const displayRows = rows.slice(-40).reverse();
3178
- 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>` : '';
3179
- 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>' +
3180
- overflowNote +
3181
- '<div id="routing-rows">' +
3182
- displayRows.map(r => {
3174
+ if (!rows.length) { pane.innerHTML = '<div class="empty">No routing data</div>'; return; }
3175
+ pane.innerHTML = '<div class="m-group-title" style="margin-bottom:6px">Routing Feedback</div>' +
3176
+ rows.slice(-40).reverse().map(r => {
3183
3177
  const agent = r.suggestedAgent || r.route || r.category || r.agent || '—';
3184
3178
  const task = r.task || r.prompt || r.description || r.sessionId?.slice(0, 8) || '—';
3185
3179
  const ts = r.timestamp || r.ts || r.created_at;
3186
3180
  const conf = r.confidence != null ? Math.round(r.confidence * 100) + '%' : '';
3187
- 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)}">
3181
+ return `<div style="padding:5px 0;border-bottom:1px solid var(--border);font-size:11px;font-family:monospace">
3188
3182
  <div style="color:var(--text-hi);display:flex;align-items:center;gap:8px">
3189
3183
  <span style="color:var(--accent)">${esc(agent)}</span>
3190
3184
  ${conf ? `<span style="color:var(--text-lo)">${esc(conf)}</span>` : ''}
3191
- <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>
3185
+ <span style="color:var(--text-lo);margin-left:auto;font-size:10px">${relTime(ts)}</span>
3192
3186
  </div>
3193
3187
  <div style="color:var(--text-lo);margin-top:1px">${esc(task)}</div>
3194
3188
  </div>`;
3195
- }).join('') + '</div>';
3189
+ }).join('');
3196
3190
  } catch (_) { pane.innerHTML = '<div class="empty">Failed to load routing data</div>'; }
3197
3191
  }
3198
3192
 
3199
- function filterRouting(q) {
3200
- const lq = (q || '').toLowerCase();
3201
- document.querySelectorAll('#routing-rows .routing-entry').forEach(el => {
3202
- const agent = (el.dataset.agent || '').toLowerCase();
3203
- const task = (el.dataset.task || '').toLowerCase();
3204
- el.style.display = (!lq || agent.includes(lq) || task.includes(lq)) ? '' : 'none';
3205
- });
3206
- }
3207
-
3208
- function filterLoopList(q) {
3209
- const lq = (q || '').toLowerCase();
3210
- document.querySelectorAll('#loops-content .loop-row').forEach(el => {
3211
- const text = (el.textContent || '').toLowerCase();
3212
- const expand = el.nextElementSibling;
3213
- const visible = !lq || text.includes(lq);
3214
- el.style.display = visible ? '' : 'none';
3215
- if (expand && expand.classList.contains('loop-expand')) expand.style.display = 'none';
3216
- });
3217
- }
3218
-
3219
- function filterOrgList(q) {
3220
- const lq = (q || '').toLowerCase();
3221
- document.querySelectorAll('#orgs-list-scroll .org-item').forEach(el => {
3222
- const name = (el.dataset.org || '').toLowerCase();
3223
- const goal = (el.querySelector('.oi-goal')?.textContent || '').toLowerCase();
3224
- el.style.display = (!lq || name.includes(lq) || goal.includes(lq)) ? '' : 'none';
3225
- });
3226
- }
3227
-
3228
3193
  async function loadMemUsage() {
3229
3194
  const pane = document.getElementById('mem-tab-usage');
3230
3195
  if (!pane) return;
@@ -3232,10 +3197,10 @@ async function loadMemUsage() {
3232
3197
  // Period tabs
3233
3198
  pane.innerHTML = `
3234
3199
  <div class="tok-periods" style="margin-bottom:14px">
3235
- <button class="tok-period-btn active" data-period="today" title="Show today's memory usage" onclick="loadMemUsagePeriod(this,'today')">Today</button>
3236
- <button class="tok-period-btn" data-period="week" title="Show this week's memory usage" onclick="loadMemUsagePeriod(this,'week')">Week</button>
3237
- <button class="tok-period-btn" data-period="30d" title="Show last 30 days of memory usage" onclick="loadMemUsagePeriod(this,'30d')">30 Days</button>
3238
- <button class="tok-period-btn" data-period="month" title="Show this month's memory usage" onclick="loadMemUsagePeriod(this,'month')">Month</button>
3200
+ <button class="tok-period-btn active" data-period="today" onclick="loadMemUsagePeriod(this,'today')">Today</button>
3201
+ <button class="tok-period-btn" data-period="week" onclick="loadMemUsagePeriod(this,'week')">Week</button>
3202
+ <button class="tok-period-btn" data-period="30d" onclick="loadMemUsagePeriod(this,'30d')">30 Days</button>
3203
+ <button class="tok-period-btn" data-period="month" onclick="loadMemUsagePeriod(this,'month')">Month</button>
3239
3204
  </div>
3240
3205
  <div id="mem-usage-content"><div class="loading-txt">Loading…</div></div>
3241
3206
  `;
@@ -3260,25 +3225,19 @@ async function loadMemUsagePeriod(btn, period) {
3260
3225
 
3261
3226
  function barChart(items, valKey, labelKey, color, maxItems) {
3262
3227
  if (!items.length) return '<div class="empty" style="font-size:12px">No data</div>';
3263
- const shown = items.slice(0, maxItems);
3264
- const maxVal = Math.max(...shown.map(x => Number(x[valKey] || 0)), 0.0001);
3265
- const rows = shown.map(item => {
3228
+ const maxVal = Math.max(...items.slice(0, maxItems).map(x => Number(x[valKey] || 0)), 0.0001);
3229
+ return items.slice(0, maxItems).map(item => {
3266
3230
  const pct = Math.round((Number(item[valKey] || 0) / maxVal) * 100);
3267
- const fullLabel = String(item[labelKey] || '—');
3268
- const label = esc(fullLabel.slice(0, 24));
3231
+ const label = esc(String(item[labelKey] || '—').slice(0, 24));
3269
3232
  const val = typeof item[valKey] === 'number' && valKey === 'cost'
3270
3233
  ? '$' + Number(item[valKey]).toFixed(4)
3271
3234
  : String(item[valKey] || 0);
3272
3235
  return `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">
3273
- <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>
3274
- <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>
3236
+ <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>
3237
+ <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>
3275
3238
  <div style="width:60px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">${esc(val)}</div>
3276
3239
  </div>`;
3277
3240
  }).join('');
3278
- const overflow = items.length > maxItems
3279
- ? `<div style="font-size:11px;color:var(--text-xs);margin-top:3px;text-align:right">Showing ${maxItems} of ${items.length}</div>`
3280
- : '';
3281
- return rows + overflow;
3282
3241
  }
3283
3242
 
3284
3243
  const totalCost = typeof s.todayCost === 'number' ? s.todayCost : (typeof s.cost === 'number' ? s.cost : null);
@@ -3321,12 +3280,11 @@ async function loadMemUsagePeriod(btn, period) {
3321
3280
  <div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px">
3322
3281
  <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>
3323
3282
  <tbody>${rows.slice(0,30).map(r => `<tr style="border-top:1px solid var(--border)">
3324
- <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>
3325
- <td style="padding:3px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>
3283
+ <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>
3284
+ <td style="padding:3px 8px;color:var(--text-lo)">${r.calls??'—'}</td>
3326
3285
  <td style="padding:3px 8px;color:var(--accent)">$${Number(r.cost??0).toFixed(4)}</td>
3327
3286
  </tr>`).join('')}</tbody>
3328
- </table></div>
3329
- ${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>` : ''}` : ''}
3287
+ </table></div>` : ''}
3330
3288
  `;
3331
3289
  } catch (e) {
3332
3290
  content.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>';
@@ -3348,15 +3306,14 @@ async function loadMemADRs() {
3348
3306
  function renderADRs(list) {
3349
3307
  const pane = document.getElementById('adr-content');
3350
3308
  if (!pane) return;
3351
- 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; }
3309
+ if (!list.length) { pane.innerHTML = '<div class="empty">No ADRs found</div>'; return; }
3352
3310
  pane.innerHTML = list.slice(0, 50).map(a => `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
3353
3311
  <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px">
3354
3312
  <span style="color:var(--text-hi);font-size:12px;font-family:monospace">${esc(a.id || a.title || '—')}</span>
3355
3313
  <span class="ss-pill ${a.status === 'accepted' ? 'on' : a.status === 'deprecated' ? 'warn' : ''}">${esc(a.status || '?')}</span>
3356
3314
  </div>
3357
- <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>
3358
- </div>`).join('') +
3359
- (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>` : '');
3315
+ <div style="font-size:11px;color:var(--text-lo)">${esc((a.context || a.summary || '').slice(0, 120))}</div>
3316
+ </div>`).join('');
3360
3317
  }
3361
3318
 
3362
3319
  function filterADRs(q) {
@@ -3371,15 +3328,11 @@ function filterADRs(q) {
3371
3328
 
3372
3329
  function renderMiniSessions(sessions) {
3373
3330
  if (!sessions.length) return;
3374
- const items = sessions.map((s, i) => {
3375
- const costStr = typeof s.totalCost === 'number' && s.totalCost > 0.001 ? ' · $' + s.totalCost.toFixed(2) : '';
3376
- const durStr = s.totalDurationMs ? ' · ' + fmtDur(s.totalDurationMs) : '';
3377
- return `
3331
+ const items = sessions.map((s, i) => `
3378
3332
  <div class="mini-sess" onclick="sessionIdx=${i};userScrolled=false;loadFeedForSession(allSessions[${i}])">
3379
- <div class="ms-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
3380
- <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>
3381
- </div>`;
3382
- }).join('');
3333
+ <div class="ms-prompt">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
3334
+ <div class="ms-meta">${relTime(s.lastTs || s.mtime)}</div>
3335
+ </div>`).join('');
3383
3336
  document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
3384
3337
  buildSwimlane();
3385
3338
  }
@@ -3406,7 +3359,7 @@ function renderProjectGrid(projects, query) {
3406
3359
  (p.name || p.slug || '').toLowerCase().includes(query.toLowerCase()) ||
3407
3360
  (p.path || '').toLowerCase().includes(query.toLowerCase())) : projects;
3408
3361
  if (!filtered.length) {
3409
- 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>';
3362
+ el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div></div>';
3410
3363
  return;
3411
3364
  }
3412
3365
  el.className = 'proj-grid';
@@ -3417,12 +3370,12 @@ function renderProjectGrid(projects, query) {
3417
3370
  return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
3418
3371
  ${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
3419
3372
  <div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
3420
- <div class="proj-card-name" title="${esc(p.name || p.slug || '')}">${esc(p.name || p.slug)}</div>
3421
- <div class="proj-card-path" title="${esc(p.path || '')}">${esc(shortPath(p.path || ''))}</div>
3373
+ <div class="proj-card-name">${esc(p.name || p.slug)}</div>
3374
+ <div class="proj-card-path">${esc(p.path || '')}</div>
3422
3375
  <div class="proj-card-stats">
3423
3376
  <div class="proj-stat"><div class="ps-v">${p.sessionCount || 0}</div><div class="ps-l">sessions</div></div>
3424
3377
  <div class="proj-stat"><div class="ps-v">${p.memoryCount || 0}</div><div class="ps-l">memories</div></div>
3425
- ${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>` : ''}
3378
+ ${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>` : ''}
3426
3379
  </div>
3427
3380
  </div>`;
3428
3381
  }).join('');
@@ -3444,33 +3397,19 @@ async function renderSessions() {
3444
3397
  document.getElementById('sess-pg-sub').textContent =
3445
3398
  sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
3446
3399
  if (!sessions.length) {
3447
- 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>';
3400
+ el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
3448
3401
  return;
3449
3402
  }
3450
3403
  let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
3451
3404
  if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
3452
3405
  if (heatmapDateFilter) toShow = toShow.filter(s => {
3453
3406
  const t = s.lastTs || s.mtime; if (!t) return false;
3454
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString() === heatmapDateFilter;
3407
+ return new Date(typeof t === 'number' ? t : t).toDateString() === heatmapDateFilter;
3455
3408
  });
3456
3409
  // f57: file pivot filter
3457
3410
  if (filePivot) toShow = toShow.filter(s => (s.filesTouched || []).includes(filePivot));
3458
3411
  if (!toShow.length) {
3459
- let emptyMsg, emptyHint;
3460
- if (filePivot) {
3461
- emptyMsg = 'No sessions touching ' + esc(filePivot.split('/').pop());
3462
- emptyHint = 'Clear the file filter to see all sessions';
3463
- } else if (heatmapDateFilter) {
3464
- emptyMsg = 'No sessions on ' + esc(heatmapDateFilter);
3465
- emptyHint = 'Click another date on the heatmap or clear the filter';
3466
- } else if (activeTagFilter) {
3467
- emptyMsg = 'No sessions tagged "' + esc(activeTagFilter) + '"';
3468
- emptyHint = 'Clear the tag filter to see all sessions';
3469
- } else {
3470
- emptyMsg = 'No bookmarked sessions';
3471
- emptyHint = 'Click the ☆ on any session row to bookmark it.';
3472
- }
3473
- 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>`;
3412
+ el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
3474
3413
  buildSessionHeatmap(sessions);
3475
3414
  return;
3476
3415
  }
@@ -3517,7 +3456,7 @@ async function renderSessions() {
3517
3456
  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('');
3518
3457
  const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
3519
3458
  const isStarred = bookmarks.has(s.id);
3520
- const sData = JSON.stringify(s).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
3459
+ const sData = JSON.stringify(s).replace(/'/g, '&#39;');
3521
3460
  const note = getSessNote(s.id);
3522
3461
  const hasNote = !!note;
3523
3462
  const files = (s.filesTouched || []).slice(0, 5);
@@ -3542,14 +3481,12 @@ async function renderSessions() {
3542
3481
  ? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
3543
3482
  : '';
3544
3483
  const summaryHtml = s.summary
3545
- ? `<div class="sr-summary" title="${esc(s.summary)}">${esc(s.summary.slice(0, 180))}${s.summary.length > 180 ? '…' : ''}</div>`
3484
+ ? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
3546
3485
  : '';
3547
- const srTimeTs = s.lastTs || s.mtime;
3548
- const srTimeFull = srTimeTs ? new Date(typeof srTimeTs === 'number' ? srTimeTs : Number(srTimeTs) || srTimeTs).toLocaleString() : '';
3549
3486
  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}'>
3550
3487
  <div class="sr-top">
3551
- <div class="sr-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3552
- <div class="sr-time"${srTimeFull ? ` title="${esc(srTimeFull)}"` : ''}>${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
3488
+ <div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3489
+ <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
3553
3490
  <button class="sr-copy-btn" data-prompt="${esc(s.lastPrompt || s.id)}" onclick="copyPrompt(this.dataset.prompt,event)" title="Copy prompt to clipboard">⎘</button>
3554
3491
  <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>
3555
3492
  <span class="sr-view">→ view</span>
@@ -3563,7 +3500,7 @@ async function renderSessions() {
3563
3500
  ${ctxGauge}
3564
3501
  <div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
3565
3502
  <div class="sess-notes-wrap" onclick="event.stopPropagation()">
3566
- <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>
3503
+ <button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
3567
3504
  <div class="sess-notes-area" id="snote-${esc(s.id)}">
3568
3505
  <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>
3569
3506
  <div class="sess-note-saved"></div>
@@ -3604,14 +3541,14 @@ async function renderSessions() {
3604
3541
  const todayCost = allSessions.filter(s => {
3605
3542
  const t = s.firstTs || s.mtime;
3606
3543
  if (!t) return false;
3607
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
3544
+ const d = new Date(typeof t === 'number' ? t : t);
3608
3545
  const now = new Date();
3609
3546
  return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
3610
3547
  }).reduce((a, s) => a + (s.totalCost || 0), 0);
3611
3548
  const monthCost = allSessions.filter(s => {
3612
3549
  const t = s.firstTs || s.mtime;
3613
3550
  if (!t) return false;
3614
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
3551
+ const d = new Date(typeof t === 'number' ? t : t);
3615
3552
  const now = new Date();
3616
3553
  return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
3617
3554
  }).reduce((a, s) => a + (s.totalCost || 0), 0);
@@ -3690,7 +3627,7 @@ function buildTagFilterBar(sessions) {
3690
3627
  if (!allTags.common.size) return '';
3691
3628
  const sorted = [...allTags.common].sort();
3692
3629
  const chips = sorted.map(t =>
3693
- `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" title="Filter sessions by tag: ${esc(t)}" data-tag="${esc(t)}" onclick="setTagFilter(this.dataset.tag)">${esc(t)}</button>`
3630
+ `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
3694
3631
  ).join('');
3695
3632
  return `<div class="tag-filter-bar">${chips}</div>`;
3696
3633
  }
@@ -3719,12 +3656,10 @@ function buildRecap(events, sess) {
3719
3656
  const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
3720
3657
 
3721
3658
  const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
3722
- const durStr = sess?.totalDurationMs ? fmtDur(sess.totalDurationMs) : null;
3723
3659
 
3724
3660
  const stats = [
3725
3661
  tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
3726
3662
  users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
3727
- durStr ? `<span class="recap-stat">${durStr}</span>` : '',
3728
3663
  costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
3729
3664
  errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
3730
3665
  ].filter(Boolean).join('');
@@ -3733,74 +3668,6 @@ function buildRecap(events, sess) {
3733
3668
  recap.className = 'show';
3734
3669
  }
3735
3670
 
3736
- // ── feature 3: global feed ─────────────────────────────────
3737
- async function renderGlobalFeed() {
3738
- const el = document.getElementById('gf-content');
3739
- el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
3740
- try {
3741
- // fetch project list
3742
- const data = await apiFetch('/api/projects');
3743
- const allProjects = data?.projects || [];
3744
- const projects = allProjects.slice(0, 8);
3745
- if (!projects.length) {
3746
- 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>';
3747
- return;
3748
- }
3749
- document.getElementById('gf-sub').textContent = allProjects.length > 8
3750
- ? `Last activity across ${projects.length} of ${allProjects.length} projects`
3751
- : `Last activity across ${projects.length} project${projects.length !== 1 ? 's' : ''}`;
3752
-
3753
- // fetch sessions for each project in parallel
3754
- const results = await Promise.allSettled(
3755
- projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
3756
- );
3757
-
3758
- // flatten + sort by recency
3759
- const entries = [];
3760
- for (const r of results) {
3761
- if (r.status !== 'fulfilled') continue;
3762
- const { project, sessions } = r.value;
3763
- for (const s of sessions.slice(0, 3)) {
3764
- entries.push({ project, session: s });
3765
- }
3766
- }
3767
- entries.sort((a, b) => {
3768
- const ta = a.session.lastTs || a.session.mtime || 0;
3769
- const tb = b.session.lastTs || b.session.mtime || 0;
3770
- return (typeof tb === 'number' ? tb : Number(tb) || new Date(tb).getTime() || 0) - (typeof ta === 'number' ? ta : Number(ta) || new Date(ta).getTime() || 0);
3771
- });
3772
-
3773
- if (!entries.length) {
3774
- 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>';
3775
- return;
3776
- }
3777
-
3778
- el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
3779
- const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
3780
- const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
3781
- const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
3782
- const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
3783
- const gfCompactBadge = (s.compactCount > 0)
3784
- ? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
3785
- : '';
3786
- const gfSummaryHtml = s.summary
3787
- ? `<div class="sr-summary" title="${esc(s.summary)}">${esc(s.summary.slice(0, 180))}${s.summary.length > 180 ? '…' : ''}</div>`
3788
- : '';
3789
- return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
3790
- <div class="sr-top">
3791
- <div class="sr-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3792
- <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}
3793
- <span class="gf-proj-tag">${esc(projName)}</span>
3794
- </div>
3795
- ${gfSummaryHtml}
3796
- <div class="sr-meta">${esc(meta)}</div>
3797
- </div>`;
3798
- }).join('') + '</div>';
3799
- } catch (err) {
3800
- el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
3801
- }
3802
- }
3803
-
3804
3671
  // ── global loops (multi-project) ───────────────────────────
3805
3672
  function deduplicateLoops(loops) {
3806
3673
  const hasRepeat = loops.some(l => l.source === '_repeat.md');
@@ -3841,94 +3708,33 @@ async function renderGlobalLoops() {
3841
3708
  if (!loops.length) continue;
3842
3709
  totalLoops += loops.length;
3843
3710
  const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
3844
- const rows = loops.map((l, idx) => {
3711
+ const rows = loops.map(l => {
3845
3712
  const isTillend = l.type === 'tillend';
3846
3713
  const curRep = l.currentRep || 0;
3847
3714
  const maxReps = l.maxReps || 0;
3848
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
3715
+ const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
3849
3716
  const isHil = l.status === 'hil:pending';
3850
3717
  const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
3851
- const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
3852
- const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive &&
3853
- nextAt > 0 && nextAt <= Date.now();
3854
- const isStaledActive = isExplicitlyActive && nextAt > 0 &&
3855
- (Date.now() - nextAt) > LOOP_STALE_MS;
3856
- const isFinished = isOverdue || isStaledActive ||
3857
- (!isExplicitlyActive && maxReps > 0 && curRep >= maxReps) ||
3718
+ const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
3719
+ const isFinished = isOverdue || (maxReps > 0 && curRep >= maxReps) ||
3858
3720
  ['finished','done','complete','completed','expired'].includes(l.status);
3859
3721
  const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
3722
+ const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
3860
3723
  const intervalStr = fmtInterval(l.interval || l.schedule);
3861
- const _lp = (function(_l) {
3862
- if (_l.command) {
3863
- const flags = [];
3864
- if (_l.type && _l.type !== 'repeat') flags.push('--' + _l.type);
3865
- if (_l.maxReps) flags.push('--maxruns ' + _l.maxReps);
3866
- if (_l.wait || _l.interval) flags.push('--wait ' + (_l.wait || _l.interval * 60));
3867
- if (_l.currentRep != null) flags.push('--rep ' + _l.currentRep);
3868
- if (_l.id) flags.push('--loop ' + _l.id);
3869
- return { userPrompt: _l.prompt || '', command: _l.command, flagsStr: flags.join(' ') };
3870
- }
3871
- const full = _l.prompt || '';
3872
- const cmdM = full.match(/^(\/\S+)/);
3873
- if (!cmdM) return { userPrompt: full, command: '', flagsStr: '' };
3874
- const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
3875
- let ti = 0, fp = [];
3876
- while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
3877
- fp.push(tokens[ti++]);
3878
- if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) fp.push(tokens[ti++]);
3879
- }
3880
- return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1], flagsStr: fp.join(' ') };
3881
- })(l);
3882
- const userPrompt = _lp.userPrompt;
3883
- const cmdStr = _lp.command;
3884
- const flagsStr = _lp.flagsStr;
3885
- const fullPrompt = l.prompt || '';
3886
- const name = (l.name || userPrompt || cmdStr || 'loop').slice(0, 60);
3887
- const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
3888
- const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
3889
- const pct = (!isTillend && maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
3890
- const progBar = (!isTillend && maxReps > 0 && running)
3891
- ? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>` : '';
3892
- const runCountDisplay = isTillend
3893
- ? `run ${curRep} / ∞${maxReps > 0 ? ' (cap: ' + maxReps + ')' : ''}`
3894
- : (maxReps > 0 ? `${curRep} / ${maxReps}` : String(curRep || '—'));
3895
- const cdownSpan = nextAt
3896
- ? ` <span class="loop-cdown${nextAt - Date.now() <= 0 ? ' overdue' : ''}" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>` : '';
3897
- const stopBtn = running
3898
- ? `<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>` : '';
3899
- const typeBadge = `<span class="loop-type-badge${isTillend ? ' tillend' : ''}">${esc(l.type || 'repeat')}</span>`;
3724
+ const type = esc(l.type || 'repeat');
3900
3725
  const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
3901
3726
  const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
3902
- const hilBanner = isHil
3903
- ? `<div class="loop-hil-banner">⚠ Waiting for human response open HIL file to resume</div>` : '';
3904
- const metaParts = [intervalStr, l.description].filter(Boolean).join(' · ').slice(0, 80);
3905
- return `<div class="loop-row" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
3906
- <div class="loop-ico">${isTillend ? '∞' : '↺'}</div>
3907
- <div class="loop-body">
3908
- <div class="loop-name" title="${esc(userPrompt || fullPrompt)}">${typeBadge}${esc(name)}</div>
3909
- <div class="loop-meta">${esc(metaParts)}${cdownSpan}</div>
3910
- ${hilBanner}
3911
- ${progBar}
3912
- </div>
3913
- <div class="loop-status ${statusClass}">${statusLabel}</div>
3914
- ${stopBtn}
3727
+ const overdueSpan = isOverdue ? ' <span class="loop-cdown overdue">overdue</span>' : '';
3728
+ const metaStr = intervalStr ? esc(intervalStr) + overdueSpan : overdueSpan;
3729
+ return `<div class="loop-row" style="cursor:default">
3730
+ <div class="loop-ico">${isTillend ? '' : '↺'}</div>
3731
+ <div class="loop-body">
3732
+ <div class="loop-name" title="${esc(l.name || l.prompt || '')}"><span class="loop-type-badge${isTillend ? ' tillend' : ''}">${type}</span>${esc(name)}</div>
3733
+ <div class="loop-meta">${metaStr}</div>
3915
3734
  </div>
3916
- <div class="loop-expand">
3917
- ${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).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Prompt copied','ok'))">⎘</button></div></div>` : ''}
3918
- ${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).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Command copied','ok'))">⎘</button></div></div>` : ''}
3919
- ${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).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Flags copied','ok'))">⎘</button></div></div>` : ''}
3920
- <div class="le-row"><div class="le-lbl">Project</div><div class="le-val"><span class="gf-proj-tag">${esc(projName)}</span></div></div>
3921
- <div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${esc(l.type || 'repeat')}</div></div>
3922
- <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(intervalStr || '—')}</div></div>
3923
- <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isHil ? '⚠ hil:pending' : (running ? '● running' : (isFinished ? '✓ done' : '○ stopped'))}</div></div>
3924
- ${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>` : ''}
3925
- <div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
3926
- ${(()=>{ 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>`:''; })()}
3927
- <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>
3928
- <div class="le-row"><div class="le-lbl">${isTillend ? 'Progress' : 'Run count'}</div><div class="le-val">${esc(runCountDisplay)}</div></div>
3929
- ${l.source ? `<div class="le-row"><div class="le-lbl">Source</div><div class="le-val">${esc(l.source)}</div></div>` : ''}
3930
- ${buildLoopSparkline(l)}
3931
- </div>`;
3735
+ <span class="gf-proj-tag">${esc(projName)}</span>
3736
+ <div class="loop-status ${statusClass}">${statusLabel}</div>
3737
+ </div>`;
3932
3738
  }).join('');
3933
3739
  sections.push(`<div style="margin-bottom:18px">
3934
3740
  <div class="m-group-title" style="margin-bottom:6px">${esc(projName)}</div>
@@ -3944,7 +3750,6 @@ async function renderGlobalLoops() {
3944
3750
  return;
3945
3751
  }
3946
3752
  el.innerHTML = sections.join('');
3947
- startCountdowns();
3948
3753
  } catch (err) {
3949
3754
  el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
3950
3755
  }
@@ -4002,9 +3807,7 @@ async function renderGlobalTokens() {
4002
3807
  </tr>`).join('');
4003
3808
 
4004
3809
  const projectCount = rows.length;
4005
- document.getElementById('gt-sub').textContent = allProjects.length > 20
4006
- ? `Token usage across ${projectCount} of ${allProjects.length} projects`
4007
- : `Token usage across ${projectCount} project${projectCount !== 1 ? 's' : ''}`;
3810
+ document.getElementById('gt-sub').textContent = `Token usage across ${projectCount} project${projectCount !== 1 ? 's' : ''}`;
4008
3811
 
4009
3812
  el.innerHTML = `<div id="gt-cards" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px">${cards}</div>
4010
3813
  <div id="gt-table">
@@ -4025,6 +3828,217 @@ async function renderGlobalTokens() {
4025
3828
  }
4026
3829
  }
4027
3830
 
3831
+ // ── feature 3: global feed ─────────────────────────────────
3832
+ async function renderGlobalFeed() {
3833
+ const el = document.getElementById('gf-content');
3834
+ el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
3835
+ try {
3836
+ // fetch project list
3837
+ const data = await apiFetch('/api/projects');
3838
+ const projects = (data?.projects || []).slice(0, 8);
3839
+ if (!projects.length) {
3840
+ el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
3841
+ return;
3842
+ }
3843
+ document.getElementById('gf-sub').textContent = `Last activity across ${projects.length} projects`;
3844
+
3845
+ // fetch sessions for each project in parallel
3846
+ const results = await Promise.allSettled(
3847
+ projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
3848
+ );
3849
+
3850
+ // flatten + sort by recency
3851
+ const entries = [];
3852
+ for (const r of results) {
3853
+ if (r.status !== 'fulfilled') continue;
3854
+ const { project, sessions } = r.value;
3855
+ for (const s of sessions.slice(0, 3)) {
3856
+ entries.push({ project, session: s });
3857
+ }
3858
+ }
3859
+ entries.sort((a, b) => {
3860
+ const ta = a.session.lastTs || a.session.mtime || 0;
3861
+ const tb = b.session.lastTs || b.session.mtime || 0;
3862
+ return (typeof tb === 'number' ? tb : new Date(tb).getTime()) - (typeof ta === 'number' ? ta : new Date(ta).getTime());
3863
+ });
3864
+
3865
+ if (!entries.length) {
3866
+ el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div></div>';
3867
+ return;
3868
+ }
3869
+
3870
+ el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
3871
+ const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
3872
+ const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
3873
+ const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
3874
+ const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
3875
+ const gfCompactBadge = (s.compactCount > 0)
3876
+ ? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
3877
+ : '';
3878
+ const gfSummaryHtml = s.summary
3879
+ ? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
3880
+ : '';
3881
+ return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
3882
+ <div class="sr-top">
3883
+ <div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3884
+ <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${gfCompactBadge}
3885
+ <span class="gf-proj-tag">${esc(projName)}</span>
3886
+ </div>
3887
+ ${gfSummaryHtml}
3888
+ <div class="sr-meta">${esc(meta)}</div>
3889
+ </div>`;
3890
+ }).join('') + '</div>';
3891
+ } catch (err) {
3892
+ el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
3893
+ }
3894
+ }
3895
+
3896
+ // ── agent chat view ────────────────────────────────────────
3897
+ let chatVSessions = {};
3898
+ let chatVCurrentId = null;
3899
+ let chatVSseSource = null;
3900
+
3901
+ function initChatView() {
3902
+ loadChatViewSessions();
3903
+ if (!chatVSseSource) connectChatViewSSE();
3904
+ }
3905
+
3906
+ async function loadChatViewSessions() {
3907
+ try {
3908
+ const data = await apiFetch('/api/mastermind/sessions');
3909
+ chatVSessions = {};
3910
+ const sel = document.getElementById('chat-v-sel');
3911
+ const prev = sel.value;
3912
+ while (sel.options.length > 1) sel.remove(1);
3913
+ const sessions = Object.values(data.sessions || {});
3914
+ sessions.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
3915
+ sessions.forEach(s => {
3916
+ const opt = document.createElement('option');
3917
+ opt.value = s.id;
3918
+ const ts = s.startedAt ? new Date(s.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
3919
+ opt.textContent = (s.id.slice(0,16)) + (ts ? ' ' + ts : '') + (s.status === 'running' ? ' ●' : '');
3920
+ sel.appendChild(opt);
3921
+ chatVSessions[s.id] = s;
3922
+ });
3923
+ if (prev && chatVSessions[prev]) { sel.value = prev; }
3924
+ else {
3925
+ const running = sessions.find(s => s.status === 'running');
3926
+ if (running) { sel.value = running.id; chatVSelectSession(running.id); }
3927
+ }
3928
+ } catch(e) { console.warn('chat sessions load failed', e); }
3929
+ }
3930
+
3931
+ function chatVSelectSession(id) {
3932
+ chatVCurrentId = id;
3933
+ const feed = document.getElementById('chat-v-feed');
3934
+ const empty = document.getElementById('chat-v-empty');
3935
+ if (!id || !chatVSessions[id]) {
3936
+ feed.innerHTML = '';
3937
+ feed.appendChild(empty);
3938
+ return;
3939
+ }
3940
+ feed.innerHTML = '';
3941
+ const session = chatVSessions[id];
3942
+ const events = session.events || [];
3943
+ events.forEach(ev => appendChatViewEvent(ev, false));
3944
+ feed.scrollTop = feed.scrollHeight;
3945
+ }
3946
+
3947
+ function appendChatViewEvent(ev, animate) {
3948
+ if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId) return;
3949
+ const feed = document.getElementById('chat-v-feed');
3950
+ if (!feed) return;
3951
+ const empty = document.getElementById('chat-v-empty');
3952
+ if (empty && feed.contains(empty)) feed.removeChild(empty);
3953
+
3954
+ let el;
3955
+ const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
3956
+ if (ev.type === 'intercom') {
3957
+ el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
3958
+ } else if (ev.type === 'agent:message' || ev.type === 'agent:spawn') {
3959
+ el = mkCVAgent(ev.agent || ev.name || '?', ev.msg || ev.message || ev.role || ev.type, ts, ev.type);
3960
+ } else if (ev.type === 'session:start') {
3961
+ el = mkCVSys('Session started' + (ev.prompt ? ': ' + esc(ev.prompt.slice(0,80)) : ''), ts);
3962
+ } else if (ev.type === 'session:complete') {
3963
+ el = mkCVSys('Session complete' + (ev.status ? ' — ' + esc(ev.status) : ''), ts);
3964
+ } else if (ev.type === 'domain:dispatch') {
3965
+ el = mkCVSys('→ ' + esc(ev.domain || '') + (ev.cmd ? ': ' + esc(ev.cmd.slice(0,80)) : ''), ts);
3966
+ } else if (ev.type === 'domain:complete') {
3967
+ el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
3968
+ } else if (ev.type === 'loop:start') {
3969
+ el = mkCVSys('Loop started: ' + esc(ev.command || ''), ts);
3970
+ if (currentView === 'loops') renderLoops();
3971
+ } else if (ev.type === 'loop:complete') {
3972
+ el = mkCVSys('Loop complete: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : ''), ts);
3973
+ if (currentView === 'loops') renderLoops();
3974
+ } else if (ev.type === 'loop:tick') {
3975
+ el = mkCVSys('Loop tick: ' + esc(ev.command || ev.id || ''), ts);
3976
+ if (currentView === 'loops') renderLoops();
3977
+ } else if (ev.type === 'loop:hil') {
3978
+ el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.id || ''), ts);
3979
+ if (currentView === 'loops') renderLoops();
3980
+ } else {
3981
+ el = mkCVSys(esc(ev.type || 'event'), ts);
3982
+ }
3983
+ if (animate) el.classList.add('cv-new');
3984
+ feed.appendChild(el);
3985
+ feed.scrollTop = feed.scrollHeight;
3986
+ }
3987
+
3988
+ function mkCVSys(html, ts) {
3989
+ const d = document.createElement('div');
3990
+ d.className = 'cv-msg cv-sys';
3991
+ d.innerHTML = `<div class="cv-bub"><span class="cv-etype">SYS</span><span class="cv-text">${html}</span><span class="cv-ts">${ts}</span></div>`;
3992
+ return d;
3993
+ }
3994
+
3995
+ function mkCVAgent(name, text, ts, typeTag) {
3996
+ const d = document.createElement('div');
3997
+ d.className = 'cv-msg cv-agent';
3998
+ const tag = typeTag === 'agent:spawn' ? 'SPAWN' : 'MSG';
3999
+ d.innerHTML = `<div class="cv-bub"><span class="cv-tag">${esc(name)}</span><span class="cv-etype">${tag}</span><span class="cv-text">${esc(String(text).slice(0,200))}</span><span class="cv-ts">${ts}</span></div>`;
4000
+ return d;
4001
+ }
4002
+
4003
+ function mkCVIntercom(from, to, text, ts) {
4004
+ const d = document.createElement('div');
4005
+ d.className = 'cv-msg cv-ic';
4006
+ d.innerHTML = `<div class="cv-bub"><span class="cv-tag cv-sender">${esc(from||'?')}</span><span class="cv-arrow">→</span><span class="cv-tag cv-receiver">${esc(to||'?')}</span><span class="cv-etype">IC</span><span class="cv-text">${esc(String(text).slice(0,200))}</span><span class="cv-ts">${ts}</span></div>`;
4007
+ return d;
4008
+ }
4009
+
4010
+ function connectChatViewSSE() {
4011
+ if (chatVSseSource) return;
4012
+ const dot = document.getElementById('chat-v-live-dot');
4013
+ const lbl = document.getElementById('chat-v-live-lbl');
4014
+ chatVSseSource = new EventSource('/api/mastermind-stream');
4015
+ chatVSseSource.onopen = () => { dot && dot.classList.add('on'); lbl && (lbl.textContent = 'LIVE'); };
4016
+ chatVSseSource.onmessage = e => {
4017
+ try { handleChatViewEvent(JSON.parse(e.data)); } catch(_) {}
4018
+ };
4019
+ chatVSseSource.onerror = () => {
4020
+ dot && dot.classList.remove('on');
4021
+ lbl && (lbl.textContent = 'OFFLINE');
4022
+ chatVSseSource.close();
4023
+ chatVSseSource = null;
4024
+ setTimeout(connectChatViewSSE, 5000);
4025
+ };
4026
+ }
4027
+
4028
+ function handleChatViewEvent(ev) {
4029
+ if (!ev || !ev.session) return;
4030
+ if (!chatVSessions[ev.session]) {
4031
+ chatVSessions[ev.session] = { id: ev.session, events: [], startedAt: ev.ts, status: 'running' };
4032
+ loadChatViewSessions();
4033
+ } else {
4034
+ const s = chatVSessions[ev.session];
4035
+ s.events = s.events || [];
4036
+ s.events.push(ev);
4037
+ if (ev.type === 'session:complete') s.status = 'complete';
4038
+ }
4039
+ if (chatVCurrentId === ev.session) appendChatViewEvent(ev, true);
4040
+ }
4041
+
4028
4042
  // ── feature 4: budget cap + desktop notification ───────────
4029
4043
  let budget = (function(){ try { return JSON.parse(localStorage.getItem('mm-budget') || '{}'); } catch { return {}; } })();
4030
4044
 
@@ -4049,7 +4063,6 @@ function saveBudget() {
4049
4063
  closeBudgetModal();
4050
4064
  checkBudget(); // check immediately
4051
4065
  updateBudgetBtnStyle();
4052
- showToast('Budget saved', budget.daily || budget.monthly ? `Daily: ${budget.daily ? '$'+budget.daily : '—'} · Monthly: ${budget.monthly ? '$'+budget.monthly : '—'}` : 'Budget cleared', 'ok');
4053
4066
  }
4054
4067
 
4055
4068
  function updateBudgetBtnStyle() {
@@ -4061,36 +4074,21 @@ function updateBudgetBtnStyle() {
4061
4074
 
4062
4075
  function checkBudget() {
4063
4076
  const cost = alertState.todayCost;
4064
- const moCost = alertState.monthCost;
4065
- // Daily budget check
4066
- if (budget.daily && cost) {
4077
+ if (!cost) return;
4078
+ if (budget.daily) {
4067
4079
  const pct = cost / budget.daily;
4068
4080
  if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
4069
4081
  alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
4070
4082
  alertState.budgetCls = 'alert-crit';
4071
- updateAlerts(); return;
4072
4083
  } else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
4073
4084
  alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
4074
4085
  alertState.budgetCls = 'alert-warn';
4075
4086
  maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
4076
- updateAlerts(); return;
4077
- }
4078
- }
4079
- // Monthly budget check
4080
- if (budget.monthly && moCost) {
4081
- const mpct = moCost / budget.monthly;
4082
- if (mpct >= 1 && !dismissedAlerts.has('budget-monthly-over')) {
4083
- alertState.budgetAlert = `Monthly budget exceeded: $${moCost.toFixed(2)} / $${budget.monthly}`;
4084
- alertState.budgetCls = 'alert-crit';
4085
- updateAlerts(); return;
4086
- } else if (mpct >= 0.8 && !dismissedAlerts.has('budget-monthly-warn')) {
4087
- alertState.budgetAlert = `Approaching monthly budget: $${moCost.toFixed(2)} / $${budget.monthly}`;
4088
- alertState.budgetCls = 'alert-warn';
4089
- updateAlerts(); return;
4087
+ } else {
4088
+ alertState.budgetAlert = null;
4090
4089
  }
4090
+ updateAlerts();
4091
4091
  }
4092
- alertState.budgetAlert = null;
4093
- updateAlerts();
4094
4092
  }
4095
4093
 
4096
4094
  function maybeNotify(title, body) {
@@ -4212,7 +4210,7 @@ function buildBreakdownByName(events) {
4212
4210
  const pct = Math.round(cnt / total * 100);
4213
4211
  return `<div class="tb-row">
4214
4212
  <div class="tb-lbl" style="width:54px" title="${esc(name)}">${esc(name.length > 8 ? name.slice(0,7)+'…' : name)}</div>
4215
- <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>
4213
+ <div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
4216
4214
  <div class="tb-count">${cnt}</div>
4217
4215
  </div>`;
4218
4216
  }).join('');
@@ -4236,7 +4234,7 @@ function buildDigest() {
4236
4234
  const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
4237
4235
  const todaySessions = allSessions.filter(s => {
4238
4236
  const t = s.lastTs || s.mtime;
4239
- return t && new Date(typeof t === 'number' ? t : Number(t) || t).getTime() >= todayStart.getTime();
4237
+ return t && new Date(typeof t === 'number' ? t : t).getTime() >= todayStart.getTime();
4240
4238
  });
4241
4239
  if (!todaySessions.length) return;
4242
4240
 
@@ -4255,8 +4253,8 @@ function buildDigest() {
4255
4253
  const stats = [
4256
4254
  `${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
4257
4255
  totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
4258
- totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
4259
- totalMsgs > 0 ? `${totalMsgs.toLocaleString()} messages` : null,
4256
+ totalTools > 0 ? `${totalTools} tool calls` : null,
4257
+ totalMsgs > 0 ? `${totalMsgs} messages` : null,
4260
4258
  longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
4261
4259
  ...themes.map(t => `#${t}`),
4262
4260
  ].filter(Boolean);
@@ -4268,7 +4266,7 @@ function buildDigest() {
4268
4266
  const monthCostSoFar = allSessions.filter(s => {
4269
4267
  const t = s.firstTs || s.mtime;
4270
4268
  if (!t) return false;
4271
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
4269
+ const d = new Date(typeof t === 'number' ? t : t);
4272
4270
  return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
4273
4271
  }).reduce((a, s) => a + (s.totalCost || 0), 0);
4274
4272
  const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
@@ -4337,25 +4335,23 @@ function toggleLeaderboard() {
4337
4335
  }
4338
4336
 
4339
4337
  function renderLeaderboard() {
4340
- const all = [...allSessions]
4341
- .filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
4342
- .sort((a, b) => b.totalCost - a.totalCost);
4338
+ const all = [...allSessions].filter(s => typeof s.totalCost === 'number' && s.totalCost > 0).sort((a, b) => b.totalCost - a.totalCost);
4343
4339
  const sorted = all.slice(0, 15);
4344
4340
  const body = document.getElementById('lb-body');
4345
- const overflow = document.getElementById('lb-overflow');
4346
- 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; }
4341
+ 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; }
4347
4342
  body.innerHTML = sorted.map((s, i) => {
4348
4343
  const cost = '$' + s.totalCost.toFixed(2);
4349
4344
  const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
4350
4345
  const prompt = s.lastPrompt || s.id;
4351
4346
  return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
4352
4347
  <td class="lb-rank">${i + 1}</td>
4353
- <td class="lb-prompt">${esc(prompt.slice(0, 60))}${prompt.length > 60 ? '…' : ''}</td>
4348
+ <td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
4354
4349
  <td class="lb-cost">${cost}</td>
4355
4350
  <td class="lb-dur">${dur}</td>
4356
4351
  </tr>`;
4357
4352
  }).join('');
4358
- if (overflow) overflow.textContent = all.length > 15 ? `Showing 15 of ${all.length} sessions` : '';
4353
+ const ov = document.getElementById('lb-overflow');
4354
+ if (ov) ov.textContent = all.length > 15 ? `Showing top 15 of ${all.length} sessions` : '';
4359
4355
  }
4360
4356
 
4361
4357
  // ── feature 12: session diff ──────────────────────────────
@@ -4472,7 +4468,7 @@ async function exportSession() {
4472
4468
  const events = data.events || [];
4473
4469
  const lines = [
4474
4470
  `# Session: ${sess.lastPrompt || sess.id}`,
4475
- `> ${new Date(typeof (sess.lastTs || sess.mtime) === 'number' ? (sess.lastTs || sess.mtime) : Number(sess.lastTs || sess.mtime) || (sess.lastTs || sess.mtime)).toLocaleString()}`,
4471
+ `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`,
4476
4472
  sess.totalCost != null ? `> Cost: $${sess.totalCost.toFixed(2)}` : '',
4477
4473
  sess.totalDurationMs ? `> Duration: ${fmtDur(sess.totalDurationMs)}` : '',
4478
4474
  '',
@@ -4480,7 +4476,7 @@ async function exportSession() {
4480
4476
  for (const ev of events) {
4481
4477
  if (ev.kind === 'user' && ev.text?.trim()) {
4482
4478
  lines.push(`\n## ${ev.text.trim().slice(0, 80)}`);
4483
- if (ev.ts) lines.push(`_${new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleTimeString()}_`);
4479
+ if (ev.ts) lines.push(`_${new Date(ev.ts).toLocaleTimeString()}_`);
4484
4480
  } else if (ev.kind === 'tool') {
4485
4481
  const label = ev.label || ev.name || ev.cat;
4486
4482
  lines.push(`- \`${ev.name || ev.cat}\`${label ? ': ' + label : ''}${ev._errored ? ' ⚠ error' : ''}`);
@@ -4556,9 +4552,9 @@ function renderBurnGauge() {
4556
4552
  }
4557
4553
  const now = Date.now();
4558
4554
  // calls in last 5 min, 15 min, 60 min
4559
- const t5 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 300000).length;
4560
- const t15 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 900000).length;
4561
- const t60 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 3600000).length;
4555
+ const t5 = tools.filter(e => now - new Date(e.ts).getTime() < 300000).length;
4556
+ const t15 = tools.filter(e => now - new Date(e.ts).getTime() < 900000).length;
4557
+ const t60 = tools.filter(e => now - new Date(e.ts).getTime() < 3600000).length;
4562
4558
  const rate5 = (t5 / 5).toFixed(1); // calls/min
4563
4559
  const rate15 = (t15 / 15).toFixed(1);
4564
4560
  const rate60 = (t60 / 60).toFixed(1);
@@ -4587,7 +4583,7 @@ function buildSwimlane() {
4587
4583
  const LANE_HUES = [75, 200, 300, 150, 25, 220, 340, 120];
4588
4584
  const rows = recent.map((s, si) => {
4589
4585
  const start = s.firstTs || s.startTs || s.mtime || now;
4590
- const startMs = typeof start === 'number' ? start : Number(start) || new Date(start).getTime() || 0;
4586
+ const startMs = typeof start === 'number' ? start : new Date(start).getTime();
4591
4587
  const dur = s.totalDurationMs || 60000;
4592
4588
  const endMs = startMs + dur;
4593
4589
  const leftPct = Math.max(0, Math.min(100, ((startMs - windowStart) / windowMs) * 100));
@@ -4605,17 +4601,17 @@ function buildSwimlane() {
4605
4601
  }).join('');
4606
4602
  // dead time: find largest gap between consecutive sessions
4607
4603
  const sorted = recent.slice().sort((a, b) => {
4608
- const _aT = a.firstTs || a.mtime; const aTs = typeof _aT === 'number' ? _aT : Number(_aT) || new Date(_aT).getTime() || 0;
4609
- const _bT = b.firstTs || b.mtime; const bTs = typeof _bT === 'number' ? _bT : Number(_bT) || new Date(_bT).getTime() || 0;
4604
+ const aTs = typeof (a.firstTs || a.mtime) === 'number' ? (a.firstTs || a.mtime) : new Date(a.firstTs || a.mtime).getTime();
4605
+ const bTs = typeof (b.firstTs || b.mtime) === 'number' ? (b.firstTs || b.mtime) : new Date(b.firstTs || b.mtime).getTime();
4610
4606
  return aTs - bTs;
4611
4607
  });
4612
4608
  let maxGapMs = 0; let gapStart = 0;
4613
4609
  for (let i = 1; i < sorted.length; i++) {
4614
4610
  const prev = sorted[i - 1];
4615
4611
  const curr = sorted[i];
4616
- const _pT = prev.firstTs || prev.mtime; const prevTs = typeof _pT === 'number' ? _pT : Number(_pT) || new Date(_pT).getTime() || 0;
4612
+ const prevTs = typeof (prev.firstTs || prev.mtime) === 'number' ? (prev.firstTs || prev.mtime) : new Date(prev.firstTs || prev.mtime).getTime();
4617
4613
  const prevEnd = prevTs + (prev.totalDurationMs || 60000);
4618
- const _cT = curr.firstTs || curr.mtime; const currTs = typeof _cT === 'number' ? _cT : Number(_cT) || new Date(_cT).getTime() || 0;
4614
+ const currTs = typeof (curr.firstTs || curr.mtime) === 'number' ? (curr.firstTs || curr.mtime) : new Date(curr.firstTs || curr.mtime).getTime();
4619
4615
  const gap = currTs - prevEnd;
4620
4616
  if (gap > maxGapMs) { maxGapMs = gap; gapStart = prevEnd; }
4621
4617
  }
@@ -4649,149 +4645,103 @@ function buildLoopSparkline(l) {
4649
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>`;
4650
4646
  }
4651
4647
 
4652
- function fmtInterval(v) {
4653
- if (!v && v !== 0) return '';
4654
- if (typeof v === 'string') return v;
4655
- const m = parseInt(v);
4656
- if (isNaN(m) || m <= 0) return String(v);
4657
- if (m < 60) return m + 'm';
4658
- if (m % 60 === 0) return (m / 60) + 'h';
4659
- return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
4660
- }
4661
-
4662
4648
  function fmtCountdown(nextAt) {
4663
- const ms = parseInt(nextAt) - Date.now();
4649
+ const ms = Number(nextAt) - Date.now();
4664
4650
  if (ms <= 0) return 'overdue';
4665
- const h = Math.floor(ms / 3600000);
4666
- const m = Math.floor((ms % 3600000) / 60000);
4667
- const s = Math.floor((ms % 60000) / 1000);
4668
- if (h > 0) return `next in ${h}h ${m}m`;
4669
- return m > 0 ? `next in ${m}m ${s}s` : `next in ${s}s`;
4651
+ const s = Math.floor(ms / 1000);
4652
+ const m = Math.floor(s / 60);
4653
+ const h = Math.floor(m / 60);
4654
+ if (h > 0) return `next in ${h}h ${m % 60}m`;
4655
+ if (m > 0) return `next in ${m}m ${s % 60}s`;
4656
+ return `next in ${s}s`;
4670
4657
  }
4671
4658
 
4672
- function shortPath(p) {
4673
- if (!p) return '—';
4674
- const parts = p.replace(/\\/g, '/').split('/').filter(Boolean);
4675
- if (parts.length <= 2) return p;
4676
- return '…/' + parts.slice(-2).join('/');
4659
+ function fmtInterval(minutes) {
4660
+ if (!minutes && minutes !== 0) return '—';
4661
+ const m = typeof minutes === 'string' ? parseFloat(minutes) : minutes;
4662
+ if (isNaN(m) || m <= 0) return String(minutes);
4663
+ if (m < 60) return m + 'm';
4664
+ const h = Math.floor(m / 60), rem = Math.round(m % 60);
4665
+ return rem ? `${h}h ${rem}m` : `${h}h`;
4677
4666
  }
4678
4667
 
4679
4668
  // ── loops ──────────────────────────────────────────────────
4669
+ const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
4670
+
4680
4671
  async function renderLoops() {
4681
4672
  const el = document.getElementById('loops-content');
4682
4673
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
4683
4674
  try {
4684
4675
  const data = await apiFetch('/api/loops?dir=' + enc(DIR));
4685
- const loops = Array.isArray(data) ? data : (data.loops || []);
4686
- const hilCountL = loops.filter(l => l.status === 'hil:pending').length;
4687
- document.getElementById('bdg-loops').textContent = loops.length ? (hilCountL ? loops.length + '' : loops.length) : '';
4676
+ let loops = Array.isArray(data) ? data : (data.loops || []);
4677
+ // Dedup: if real _repeat.md loops exist, hide scheduled_tasks_lock noise
4678
+ const hasRepeatLoops = loops.some(l => l.source !== 'scheduled_tasks_lock' && l.source !== 'schedule_wakeup_hook');
4679
+ if (hasRepeatLoops) loops = loops.filter(l => l.source !== 'scheduled_tasks_lock' && l.source !== 'schedule_wakeup_hook');
4680
+ document.getElementById('bdg-loops').textContent = loops.length || '—';
4688
4681
  if (!loops.length) {
4689
- 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>';
4682
+ 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:6px">Create one with + New Loop above or via <code>npx monomind autodev --tillend</code></div></div>';
4690
4683
  return;
4691
4684
  }
4692
- // Deduplicate: hide hook/lock entries shadowed by _repeat.md entries
4693
- const hasRepeatLoops = loops.some(l => l.source === '_repeat.md');
4694
- const repeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
4695
- const dedupedLoops = loops.filter(l => {
4696
- if (l.source === 'scheduled_tasks_lock' && hasRepeatLoops) return false;
4697
- if (l.source !== 'schedule_wakeup_hook') return true;
4698
- const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
4699
- if (!m) return true;
4700
- return !repeatPrompts.has(m[1].trim());
4701
- });
4702
- el.innerHTML = dedupedLoops.map((l, idx) => {
4703
- const isHil = l.status === 'hil:pending';
4704
- const isTillend = l.type === 'tillend';
4705
- const curRep = l.currentRep || 0;
4706
- const maxReps = l.maxReps || 0;
4707
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
4708
- // Loops with status 'running'/'waiting'/'active' are explicitly active.
4709
- // Don't mark them overdue unless nextRunAt is >2h stale (loop died without cleanup).
4685
+ el.innerHTML = loops.map((l, idx) => {
4686
+ const maxReps = l.maxReps || 0;
4687
+ const curRep = l.currentRep || 0;
4688
+ const isTillend = !maxReps || l.loopType === 'tillend' || String(l.command || '').includes('--tillend');
4689
+ const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
4710
4690
  const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
4711
- const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
4712
- const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive &&
4713
- nextAt > 0 && nextAt <= Date.now();
4714
- const isStaledActive = isExplicitlyActive && nextAt > 0 &&
4715
- (Date.now() - nextAt) > LOOP_STALE_MS;
4716
- const isFinished = isOverdue || isStaledActive ||
4717
- (!isExplicitlyActive && maxReps > 0 && curRep >= maxReps) ||
4718
- l.status === 'finished' || l.status === 'done' ||
4719
- l.status === 'complete' || l.status === 'completed' || l.status === 'expired';
4720
- const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
4721
- const intervalStr = fmtInterval(l.interval || l.schedule);
4722
- // Parse loop into consistent: userPrompt / command / flags
4723
- const _lp = (function(_l) {
4724
- if (_l.command) {
4725
- const flags = [];
4726
- if (_l.type && _l.type !== 'repeat') flags.push('--' + _l.type);
4727
- if (_l.maxReps) flags.push('--maxruns ' + _l.maxReps);
4728
- if (_l.interval) { const s = String(_l.interval).match(/^(\d+)s$/); if (s) flags.push('--wait ' + s[1]); }
4729
- if (_l.currentRep != null) flags.push('--rep ' + _l.currentRep);
4730
- if (_l.id) flags.push('--loop ' + _l.id);
4731
- return { userPrompt: _l.prompt || '', command: _l.command, flagsStr: flags.join(' ') };
4732
- }
4733
- const full = _l.prompt || '';
4734
- const cmdM = full.match(/^(\/\S+)/);
4735
- if (!cmdM) return { userPrompt: full, command: '', flagsStr: '' };
4736
- const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
4737
- let ti = 0, fp = [];
4738
- while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
4739
- fp.push(tokens[ti++]);
4740
- if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) fp.push(tokens[ti++]);
4741
- }
4742
- return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1], flagsStr: fp.join(' ') };
4743
- })(l);
4744
- const userPrompt = _lp.userPrompt;
4745
- const cmdStr = _lp.command;
4746
- const flagsStr = _lp.flagsStr;
4747
- const fullPrompt = l.prompt || '';
4748
- const name = (l.name || userPrompt || cmdStr || 'loop').slice(0, 60);
4749
- const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
4750
- const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
4751
- const pct = (!isTillend && maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
4752
- const progBar = (!isTillend && maxReps > 0 && running)
4753
- ? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
4754
- : '';
4755
- const runCountDisplay = isTillend
4756
- ? `run ${curRep} / ∞${maxReps > 0 ? ' (cap: ' + maxReps + ')' : ''}`
4757
- : (maxReps > 0 ? `${curRep} / ${maxReps}` : String(curRep || '—'));
4758
- const cdownSpan = nextAt
4759
- ? ` <span class="loop-cdown${nextAt - Date.now() <= 0 ? ' overdue' : ''}" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>`
4691
+ const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
4692
+ const isStaledActive = isExplicitlyActive && nextAt > 0 && (Date.now() - nextAt) > LOOP_STALE_MS;
4693
+ const isFinished = (maxReps > 0 && curRep >= maxReps)
4694
+ || ['finished','done','complete','completed','expired'].includes(l.status)
4695
+ || isOverdue || isStaledActive;
4696
+ const isHil = l.status === 'hil:pending';
4697
+ const running = !isFinished && !isHil && l.status !== 'stopped' && l.status !== 'paused';
4698
+ const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
4699
+ const interval = fmtInterval(l.interval || l.schedule || '');
4700
+ const fullPrompt = l.prompt || '';
4701
+ const command = l.command || '';
4702
+ const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
4703
+ const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
4704
+ const runs = curRep != null ? curRep : '—';
4705
+ const pct = (maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
4706
+ const progBar = isTillend
4707
+ ? `<div class="lp-bar" title="tillend loop"><div class="lp-fill lp-fill-inf" style="width:100%;opacity:0.3;background:linear-gradient(90deg,var(--accent),transparent)"></div><span style="position:absolute;left:6px;top:0;font-size:9px;color:var(--text-lo)">run ${curRep} / ∞${l.capReps ? ' (cap: '+l.capReps+')' : ''}</span></div>`
4708
+ : (maxReps > 0 && running)
4709
+ ? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
4710
+ : '';
4711
+ const cdownSpan = (running && nextAt)
4712
+ ? ` <span class="loop-cdown" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>`
4760
4713
  : '';
4761
- const stopBtn = running
4762
- ? `<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>`
4714
+ const stopBtn = (running || isHil)
4715
+ ? `<button class="loop-stop-btn" data-loop-id="${esc(l.id || l.name || String(idx))}" onclick="stopLoop(event, this.dataset.loopId)">■ Stop</button>`
4763
4716
  : '';
4764
- const typeBadge = `<span class="loop-type-badge${isTillend ? ' tillend' : ''}">${esc(l.type || 'repeat')}</span>`;
4765
- const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
4766
- const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
4717
+ const typeBadge = isTillend
4718
+ ? `<span class="loop-type-badge" title="till-end loop">∞</span>`
4719
+ : `<span class="loop-type-badge rep" title="repeat loop">↺</span>`;
4767
4720
  const hilBanner = isHil
4768
- ? `<div class="loop-hil-banner">⚠ Waiting for human response open HIL file to resume</div>`
4721
+ ? `<div class="loop-hil-banner">⚠ Human-in-the-loop confirmation requiredcheck session for approval prompt</div>`
4769
4722
  : '';
4770
- const metaParts = [intervalStr, l.description].filter(Boolean).join(' · ').slice(0, 80);
4771
- return `<div class="loop-row" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
4772
- <div class="loop-ico">${isTillend ? '' : ''}</div>
4723
+ const statusLabel = isFinished ? 'done' : isHil ? 'HIL' : running ? 'active' : 'stopped';
4724
+ const statusCls = isFinished ? 'done' : isHil ? 'hil' : running ? 'active' : 'stopped';
4725
+ return `<div class="loop-row${isHil ? ' hil' : ''}" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
4726
+ <div class="loop-ico">${typeBadge}</div>
4773
4727
  <div class="loop-body">
4774
- <div class="loop-name" title="${esc(userPrompt || fullPrompt)}">${typeBadge}${esc(name)}</div>
4775
- <div class="loop-meta">${esc(metaParts)}${cdownSpan}</div>
4776
- ${hilBanner}
4728
+ <div class="loop-name">${esc(name)}</div>
4729
+ <div class="loop-meta">${interval !== '—' ? interval + ' · ' : ''}${esc([l.description].filter(Boolean).join('').slice(0, 80))}${cdownSpan}</div>
4777
4730
  ${progBar}
4731
+ ${hilBanner}
4778
4732
  </div>
4779
- <div class="loop-status ${statusClass}">${statusLabel}</div>
4733
+ <div class="loop-status ${statusCls}">${statusLabel}${isHil ? ' ⚠' : ''}</div>
4780
4734
  ${stopBtn}
4781
4735
  </div>
4782
4736
  <div class="loop-expand">
4783
- ${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).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Prompt copied','ok'))">⎘</button></div></div>` : ''}
4784
- ${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).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Command copied','ok'))">⎘</button></div></div>` : ''}
4785
- ${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).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Flags copied','ok'))">⎘</button></div></div>` : ''}
4786
- <div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${esc(l.type || 'repeat')}</div></div>
4787
- <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(intervalStr || '')}</div></div>
4788
- <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isHil ? '⚠ hil:pending' : (running ? '● running' : (isFinished ? '✓ done' : '○ stopped'))}</div></div>
4789
- ${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>` : ''}
4737
+ ${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(${JSON.stringify(fullPrompt)}).then(()=>showToast('Copied','','ok'))">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
4738
+ ${command && command !== fullPrompt ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(${JSON.stringify(command)}).then(()=>showToast('Copied','','ok'))">${esc(command.slice(0, 200))}</div></div>` : ''}
4739
+ <div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${isTillend ? '∞ tillend' : '↺ repeat'}</div></div>
4740
+ <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${interval}</div></div>
4741
+ <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isFinished ? '✓ done' : isHil ? '⚠ hil:pending' : running ? '● running' : '○ stopped'}</div></div>
4790
4742
  <div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
4791
- ${(()=>{ 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>`:''; })()}
4792
- <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>
4793
- <div class="le-row"><div class="le-lbl">${isTillend ? 'Progress' : 'Run count'}</div><div class="le-val">${esc(runCountDisplay)}</div></div>
4794
- ${l.source ? `<div class="le-row"><div class="le-lbl">Source</div><div class="le-val">${esc(l.source)}</div></div>` : ''}
4743
+ <div class="le-row"><div class="le-lbl">Last run</div><div class="le-val">${esc(lastRun)}</div></div>
4744
+ <div class="le-row"><div class="le-lbl">Run count</div><div class="le-val">${esc(String(runs))}${maxReps ? ' / ' + maxReps : isTillend ? ' / ∞' : ''}</div></div>
4795
4745
  ${buildLoopSparkline(l)}
4796
4746
  </div>`;
4797
4747
  }).join('');
@@ -4831,25 +4781,27 @@ async function stopLoop(evt, id) {
4831
4781
  const btn = evt.currentTarget;
4832
4782
  if (!btn.dataset.confirming) {
4833
4783
  btn.dataset.confirming = '1';
4784
+ const orig = btn.textContent;
4834
4785
  btn.textContent = '■ Confirm?';
4835
- 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)';
4836
4786
  btn._resetTimer = setTimeout(() => {
4837
4787
  delete btn.dataset.confirming;
4838
- btn.textContent = '■ Stop';
4839
- btn.style.cssText = '';
4788
+ btn.textContent = orig;
4840
4789
  }, 3000);
4841
4790
  return;
4842
4791
  }
4843
4792
  clearTimeout(btn._resetTimer);
4844
4793
  delete btn.dataset.confirming;
4845
4794
  try {
4846
- await fetch('/api/loops/stop?dir=' + enc(DIR), {
4795
+ const r = await fetch('/api/loops/stop?dir=' + enc(DIR), {
4847
4796
  method: 'POST', headers: { 'Content-Type': 'application/json' },
4848
- body: JSON.stringify({ id })
4797
+ body: JSON.stringify({ id }),
4849
4798
  });
4850
- showToast('Stopped', 'Loop stopped', 'ok');
4851
- renderLoops();
4852
- } catch (e) { showToast('Error', e.message, 'err'); }
4799
+ if (!r.ok) throw new Error('HTTP ' + r.status);
4800
+ showToast('Stopped', 'Loop stop requested', 'ok');
4801
+ setTimeout(() => renderLoops(), 400);
4802
+ } catch (e) {
4803
+ showToast('Error', e.message, 'err');
4804
+ }
4853
4805
  }
4854
4806
 
4855
4807
  let _cdownInterval = null;
@@ -4859,32 +4811,8 @@ function startCountdowns() {
4859
4811
  _cdownInterval = setInterval(() => {
4860
4812
  document.querySelectorAll('.loop-cdown[data-nextat]').forEach(el => {
4861
4813
  const ms = parseInt(el.dataset.nextat) - Date.now();
4862
- if (ms <= 0) {
4863
- const row = el.closest('.loop-row');
4864
- const loopStatus = row ? (row.dataset.loopStatus || '') : '';
4865
- const isActiveStatus = loopStatus === 'running' || loopStatus === 'waiting' || loopStatus === 'active';
4866
- if (isActiveStatus) {
4867
- // Loop is between rounds or executing — not done, just waiting for next update
4868
- el.textContent = 'executing…';
4869
- el.classList.remove('overdue');
4870
- } else {
4871
- el.textContent = 'overdue';
4872
- el.classList.add('overdue');
4873
- if (row) {
4874
- const badge = row.querySelector('.loop-status');
4875
- if (badge && badge.classList.contains('active')) {
4876
- badge.classList.remove('active');
4877
- badge.classList.add('stopped');
4878
- badge.textContent = 'done';
4879
- }
4880
- const stopBtn = row.querySelector('.loop-stop-btn');
4881
- if (stopBtn) stopBtn.remove();
4882
- }
4883
- }
4884
- return;
4885
- }
4886
4814
  el.textContent = fmtCountdown(el.dataset.nextat);
4887
- el.classList.remove('overdue');
4815
+ el.classList.toggle('overdue', ms <= 0);
4888
4816
  });
4889
4817
  }, 1000);
4890
4818
  }
@@ -4936,7 +4864,7 @@ function buildEfficiencyPanel() {
4936
4864
  <span class="eff-lbl" title="${esc(s.lastPrompt||s.id)}">${esc(lbl)}</span>
4937
4865
  <span class="eff-pct ${cls}">${pct}%</span>
4938
4866
  </div>
4939
- <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>`;
4867
+ <div class="eff-bar-wrap"><div class="eff-bar-fill" style="width:${pct}%;background:${fillColor}"></div></div>`;
4940
4868
  }).join('');
4941
4869
  el.innerHTML = `<div class="m-group-title">Cache Efficiency <span class="${avgCls}" style="font-size:10px;font-weight:400">${avgPct}% avg</span></div>${rows}`;
4942
4870
  }
@@ -4973,10 +4901,10 @@ function renderModelMix() {
4973
4901
  const short = model.replace(/^claude-/,'').replace(/-\d{8}$/,'');
4974
4902
  const pct = totalCost > 0 ? Math.round(d.cost/totalCost*100) : 0;
4975
4903
  return `<tr>
4976
- <td style="font-size:11px" title="${esc(model)}">${esc(short)}</td>
4904
+ <td style="font-size:11px">${esc(short)}</td>
4977
4905
  <td class="lb-cost">$${d.cost.toFixed(2)}</td>
4978
4906
  <td class="lb-dur">${pct}%</td>
4979
- <td class="lb-dur">${d.calls.toLocaleString()}</td>
4907
+ <td class="lb-dur">${d.calls}</td>
4980
4908
  </tr>`;
4981
4909
  }).join('')}
4982
4910
  </tbody></table>`;
@@ -4995,14 +4923,14 @@ function buildWeeklyRecap() {
4995
4923
  const weekStart = new Date(); weekStart.setDate(weekStart.getDate() - weekStart.getDay()); weekStart.setHours(0,0,0,0);
4996
4924
  const weekSess = allSessions.filter(s => {
4997
4925
  const t = s.lastTs || s.mtime;
4998
- return t && new Date(typeof t === 'number' ? t : Number(t) || t).getTime() >= weekStart.getTime();
4926
+ return t && new Date(typeof t === 'number' ? t : t).getTime() >= weekStart.getTime();
4999
4927
  });
5000
4928
  if (!weekSess.length) return;
5001
4929
  const totalCost = weekSess.reduce((a,s) => a + (s.totalCost||0), 0);
5002
4930
  const totalTools = weekSess.reduce((a,s) => a + (s.toolCalls||0), 0);
5003
4931
  const days = new Set(weekSess.map(s => {
5004
4932
  const t = s.lastTs || s.mtime;
5005
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString();
4933
+ return new Date(typeof t === 'number' ? t : t).toDateString();
5006
4934
  })).size;
5007
4935
  const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
5008
4936
  const streak = calcStreak();
@@ -5070,12 +4998,11 @@ function buildActivityHeatmap() {
5070
4998
  for (const s of allSessions) {
5071
4999
  const t = s.firstTs || s.mtime;
5072
5000
  if (!t) continue;
5073
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
5001
+ const d = new Date(typeof t === 'number' ? t : t);
5074
5002
  grid[d.getDay()][d.getHours()]++;
5075
5003
  }
5076
5004
  const maxVal = Math.max(1, ...grid.flat());
5077
5005
  const DAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];
5078
- const FULL_DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
5079
5006
  let html = '<div class="heatmap-grid">';
5080
5007
  html += '<div class="heatmap-row"><div class="heatmap-lbl"></div>';
5081
5008
  for (let h = 0; h < 24; h++) {
@@ -5088,7 +5015,7 @@ function buildActivityHeatmap() {
5088
5015
  const v = grid[d][h];
5089
5016
  const alpha = v > 0 ? Math.max(0.18, v/maxVal).toFixed(2) : 0;
5090
5017
  const bg = v > 0 ? `oklch(65% 0.18 200 / ${alpha})` : 'transparent';
5091
- 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>`;
5018
+ 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>`;
5092
5019
  }
5093
5020
  html += '</div>';
5094
5021
  }
@@ -5170,8 +5097,8 @@ function v2RenderOrgList() {
5170
5097
  return `<div class="org-item${active}" data-org="${esc(o.name)}" onclick="v2SelectOrg(this.dataset.org)">
5171
5098
  <div class="oi-dot ${o.running ? 'running' : ''}"></div>
5172
5099
  <div class="oi-body">
5173
- <div class="oi-name" title="${esc(o.name)}">${esc(o.name)}</div>
5174
- ${goalSnip ? `<div class="oi-goal" title="${esc(o.goal || goalSnip)}">${esc(goalSnip)}</div>` : ''}
5100
+ <div class="oi-name">${esc(o.name)}</div>
5101
+ ${goalSnip ? `<div class="oi-goal">${esc(goalSnip)}</div>` : ''}
5175
5102
  <div class="oi-chips">
5176
5103
  ${o.running ? '<span class="oi-chip live">LIVE</span>' : ''}
5177
5104
  <span class="oi-chip">${rolesN} roles</span>
@@ -5637,7 +5564,7 @@ function v2RenderOrgRoles() {
5637
5564
  const pane = document.getElementById('odt-roles');
5638
5565
  if (!pane) return;
5639
5566
  if (!roles.length) {
5640
- 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>';
5567
+ pane.innerHTML = '<div class="empty">No roles defined</div>';
5641
5568
  return;
5642
5569
  }
5643
5570
  // Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
@@ -5709,7 +5636,7 @@ function v2RenderAgentDrawer(data) {
5709
5636
  headEl.innerHTML = `
5710
5637
  <img class="oad-avatar" src="${avatar}" alt="${esc(name)}" onerror="this.src='data/avatars/coder.svg'"/>
5711
5638
  <div class="oad-id">
5712
- <div class="oad-name" title="${esc(name)}">${esc(name)}</div>
5639
+ <div class="oad-name">${esc(name)}</div>
5713
5640
  <div class="oad-pills">
5714
5641
  ${type ? `<span class="oad-pill">${esc(type)}</span>` : ''}
5715
5642
  ${model ? `<span class="oad-pill model">${esc(model)}</span>` : ''}
@@ -5787,7 +5714,7 @@ function v2RenderOrgActivity() {
5787
5714
  if (!_v2SelOrg) return;
5788
5715
  const activity = _v2OrgData?._activity || [];
5789
5716
  const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
5790
- const events = [...activity, ...orgEvents].sort((a,b) => (Number(b.ts)||0)-(Number(a.ts)||0)).slice(0,80);
5717
+ const events = [...activity, ...orgEvents].sort((a,b) => (b.ts||0)-(a.ts||0)).slice(0,80);
5791
5718
  const pane = document.getElementById('odt-activity');
5792
5719
  if (!pane) return;
5793
5720
  const fmtOrgEvType = t => {
@@ -5795,14 +5722,13 @@ function v2RenderOrgActivity() {
5795
5722
  return m[t]||(t||'').replace(/^org:/,'');
5796
5723
  };
5797
5724
  if (!events.length) {
5798
- 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>';
5725
+ pane.innerHTML = '<div class="empty">No activity recorded</div>';
5799
5726
  return;
5800
5727
  }
5801
5728
  pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
5802
- 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'}) : '';
5729
+ const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
5803
5730
  const detail = ev.role||ev.msg||ev.agent||'';
5804
- const tFull = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '';
5805
- 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>`;
5731
+ 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>`;
5806
5732
  }).join('')}</div>`;
5807
5733
  }
5808
5734
 
@@ -5840,12 +5766,12 @@ function v2RenderOrgHeartbeats() {
5840
5766
  status: a.status || 'idle',
5841
5767
  }));
5842
5768
  }
5843
- 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; }
5769
+ if (!hb.length) { pane.innerHTML = '<div class="empty">No agents to report</div>'; return; }
5844
5770
  const cls = (st) => (st === 'active' || st === 'running') ? 'on' : ((st === 'error' || st === 'failed') ? 'warn' : '');
5845
5771
  pane.innerHTML = `<div class="m-group-title">Agent Heartbeats</div>` +
5846
5772
  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">
5847
- <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>
5848
- <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>
5773
+ <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>
5774
+ <span style="color:var(--text-lo);white-space:nowrap">${h.ts ? relTime(h.ts) : 'never'}</span>
5849
5775
  <span class="ss-pill ${cls(h.status)}">${esc(String(h.status || 'idle').toUpperCase())}</span>
5850
5776
  </div>`).join('');
5851
5777
  }
@@ -5864,15 +5790,15 @@ function v2RenderOrgTasks() {
5864
5790
  (Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: t.status || col }));
5865
5791
  }
5866
5792
  }
5867
- 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; }
5793
+ if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks</div>'; return; }
5868
5794
  const rank = { running: 0, doing: 0, todo: 1, pending: 1, done: 2 };
5869
5795
  tasks.sort((a, b) => (rank[a.status] ?? 1) - (rank[b.status] ?? 1));
5870
5796
  const pill = (st) => st === 'done' ? 'on' : (st === 'running' || st === 'doing' ? 'warn' : '');
5871
5797
  pane.innerHTML = `<div class="m-group-title">Tasks (${tasks.length})</div>` +
5872
5798
  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">
5873
5799
  <span class="ss-pill ${pill(t.status)}">${esc(t.status || '?')}</span>
5874
- <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>
5875
- <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>
5800
+ <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>
5801
+ <span style="color:var(--text-lo);white-space:nowrap">${relTime(t.ts || t.created_at || t.updated_at)}</span>
5876
5802
  </div>`).join('');
5877
5803
  }
5878
5804
 
@@ -5896,7 +5822,7 @@ function v2RenderOrgCosts() {
5896
5822
  ? c.map(r => ({ label: r.label ?? r.name ?? '—', cost: Number(r.value ?? r.cost ?? 0), tin: 0, tout: 0 }))
5897
5823
  : Object.entries(c).map(([k, v]) => ({ label: k, cost: Number(v) || 0, tin: 0, tout: 0 }));
5898
5824
  }
5899
- 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; }
5825
+ if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data</div>'; return; }
5900
5826
  const cur = (b && b.currency) || 'USD';
5901
5827
  const period = (b && b.period) || '';
5902
5828
  const total = rows.reduce((s, r) => s + r.cost, 0);
@@ -5908,7 +5834,7 @@ function v2RenderOrgCosts() {
5908
5834
  <span style="color:var(--accent);font-family:var(--mono);font-size:15px;font-weight:600">$${total.toFixed(4)}</span>
5909
5835
  </div>` +
5910
5836
  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">
5911
- <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>
5837
+ <span style="color:var(--text-hi);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(String(r.label))}</span>
5912
5838
  <span style="color:var(--text-lo);font-family:var(--mono);font-size:10px;white-space:nowrap">${(r.tin + r.tout).toLocaleString()} tok</span>
5913
5839
  <span style="color:var(--accent);font-family:var(--mono);white-space:nowrap">$${r.cost.toFixed(4)}</span>
5914
5840
  </div>`).join('') +
@@ -5925,14 +5851,14 @@ function v2RenderOrgMembers() {
5925
5851
  const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
5926
5852
  const list = members.length ? members : roles;
5927
5853
  const joinReqs = Array.isArray(_v2OrgData?._joinRequests) ? _v2OrgData._joinRequests : [];
5928
- 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; }
5854
+ if (!list.length) { pane.innerHTML = '<div class="empty">No members</div>'; return; }
5929
5855
  const src = members.length ? 'joined members' : 'defined roles';
5930
5856
  const active = (r) => r.running || r.active || r.status === 'active';
5931
5857
  pane.innerHTML = `<div class="m-group-title">Members (${list.length}) · ${src}</div>` +
5932
5858
  list.map(r => `<div style="display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
5933
5859
  <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>
5934
5860
  <div style="flex:1;min-width:0">
5935
- <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>
5861
+ <div style="color:var(--text-hi);font-family:var(--mono);font-size:12px">${esc(r.name || r.id || r.user || '—')}</div>
5936
5862
  <div style="color:var(--text-lo);font-size:10px">${esc(r.title || r.type || r.role || '')}</div>
5937
5863
  </div>
5938
5864
  <span class="ss-pill ${active(r) ? 'on' : ''}">${active(r) ? 'ACTIVE' : 'IDLE'}</span>
@@ -5946,7 +5872,7 @@ function v2RenderOrgGoals() {
5946
5872
  const el = document.getElementById('odt-goals');
5947
5873
  if (!el || !_v2OrgData) return;
5948
5874
  const goals = _v2OrgData.goals || _v2OrgData.config?.goals || [];
5949
- 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; }
5875
+ if (!goals.length) { el.innerHTML = '<div class="empty">No goals defined</div>'; return; }
5950
5876
  function renderGoal(g, depth) {
5951
5877
  if (depth > 20) return ''; // Depth guard to prevent stack overflow
5952
5878
  const indent = depth * 20;
@@ -5970,7 +5896,7 @@ function v2RenderOrgBoard() {
5970
5896
  const el = document.getElementById('odt-board');
5971
5897
  if (!el || !_v2OrgData) return;
5972
5898
  const issues = _v2OrgData._issues || [];
5973
- 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; }
5899
+ if (!issues.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
5974
5900
  const cols = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
5975
5901
  const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
5976
5902
  el.innerHTML = `<div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">` +
@@ -5979,10 +5905,9 @@ function v2RenderOrgBoard() {
5979
5905
  return `<div style="min-width:160px;flex:1">
5980
5906
  <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>
5981
5907
  ${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">
5982
- <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>
5908
+ <div style="color:var(--text-hi)">${PRIORITY[i.priority] || ''} ${esc((i.title || i.description || '—').slice(0, 60))}</div>
5983
5909
  ${i.assignee ? `<div style="font-size:10px;color:var(--text-lo);margin-top:3px">${esc(i.assignee)}</div>` : ''}
5984
5910
  </div>`).join('')}
5985
- ${cards.length > 15 ? `<div style="font-size:11px;color:var(--text-xs);text-align:center;padding:4px 0">+${cards.length - 15} more</div>` : ''}
5986
5911
  </div>`;
5987
5912
  }).join('') + `</div>`;
5988
5913
  }
@@ -6006,9 +5931,9 @@ function v2RenderOrgLive() {
6006
5931
  <div class="m-group-title" style="margin-bottom:6px">Activity Feed</div>
6007
5932
  <div style="max-height:240px;overflow-y:auto;font-size:11px;font-family:var(--mono)">
6008
5933
  ${(_v2OrgData._activity || []).slice(-30).reverse().map(e => `<div style="padding:3px 0;border-bottom:1px solid var(--border);color:var(--text-lo)">
6009
- <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>
5934
+ ${esc(relTime(e.ts || e.timestamp || e.created_at))}
6010
5935
  <span style="color:var(--text-mid);margin-left:6px">${esc(e.type || e.kind || e.event || '—')}</span>
6011
- ${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>` : ''}
5936
+ ${e.message ? `<span style="color:var(--text-hi);margin-left:6px">${esc(e.message.toString().slice(0, 80))}</span>` : ''}
6012
5937
  </div>`).join('')}
6013
5938
  </div>`;
6014
5939
  // auto-refresh every 5s while tab is active
@@ -6033,7 +5958,7 @@ async function v2RenderOrgApprovals() {
6033
5958
  const _enc = encodeURIComponent(_v2SelOrg);
6034
5959
  const data = await fetch(`/api/org/${_enc}/approvals${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6035
5960
  const approvals = Array.isArray(data) ? data : (data.approvals || []);
6036
- 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; }
5961
+ if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals</div>'; return; }
6037
5962
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6038
5963
  <thead><tr style="color:var(--text-xs);text-align:left">
6039
5964
  <th style="padding:6px 8px">Requester</th><th>Action</th><th>Status</th><th>Date</th><th></th>
@@ -6044,13 +5969,13 @@ async function v2RenderOrgApprovals() {
6044
5969
  const aid = a.id || '';
6045
5970
  return `<tr style="border-top:1px solid var(--border)">
6046
5971
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(a.requester || a.agent || '—')}</td>
6047
- <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>
5972
+ <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>
6048
5973
  <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(a.status || 'pending')}</span></td>
6049
- <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>
5974
+ <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
6050
5975
  <td style="padding:6px 8px;white-space:nowrap">
6051
5976
  ${pending
6052
- ? `<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>
6053
- <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>`
5977
+ ? `<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>
5978
+ <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>`
6054
5979
  : ''}
6055
5980
  </td>
6056
5981
  </tr>`;
@@ -6086,7 +6011,7 @@ async function v2RenderOrgSecrets() {
6086
6011
  ${s.purpose ? `<span style="color:var(--text-lo)">${esc(s.purpose)}</span>` : ''}
6087
6012
  <span style="margin-left:auto;font-family:var(--mono);color:var(--border)">••••••••</span>
6088
6013
  </div>`).join('')
6089
- : '<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>');
6014
+ : '<div class="empty">No secrets configured</div>');
6090
6015
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
6091
6016
  }
6092
6017
 
@@ -6103,19 +6028,19 @@ function v2RenderOrgSettings() {
6103
6028
  el.innerHTML = `
6104
6029
  <div style="font-size:11px;color:var(--text-lo);margin-bottom:14px">Changes generate a CLI command — no direct writes from UI.</div>
6105
6030
  <div style="display:flex;flex-direction:column;gap:12px;max-width:380px">
6106
- <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>
6031
+ <div><div class="le-lbl">Goal</div><input id="os-goal" class="filter-input" value="${esc(d.goal || '')}"></div>
6107
6032
  <div><div class="le-lbl">Topology</div>
6108
- <select id="os-topo" class="filter-input" title="Agent coordination topology" style="cursor:pointer">
6033
+ <select id="os-topo" class="filter-input" style="cursor:pointer">
6109
6034
  ${topos.map(t => `<option ${d.topology === t ? 'selected' : ''}>${esc(t)}</option>`).join('')}
6110
6035
  </select>
6111
6036
  </div>
6112
6037
  <div><div class="le-lbl">Governance</div>
6113
- <select id="os-gov" class="filter-input" title="Governance / consensus strategy" style="cursor:pointer">
6038
+ <select id="os-gov" class="filter-input" style="cursor:pointer">
6114
6039
  ${govs.map(g => `<option ${govVal === g ? 'selected' : ''}>${esc(g)}</option>`).join('')}
6115
6040
  </select>
6116
6041
  </div>
6117
- <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>
6118
- <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>
6042
+ <div><div class="le-lbl">Budget (tokens)</div><input id="os-budget" class="filter-input" type="number" value="${esc(String(budgetVal || ''))}"></div>
6043
+ <button class="btn" style="width:fit-content;color:var(--accent);border-color:var(--accent)" onclick="generateOrgSettingsCmd()">Generate CLI Command</button>
6119
6044
  <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>
6120
6045
  </div>`;
6121
6046
  }
@@ -6145,7 +6070,7 @@ async function v2RenderOrgRoutines() {
6145
6070
  const _enc = encodeURIComponent(_v2SelOrg);
6146
6071
  const data = await fetch(`/api/org/${_enc}/routines${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6147
6072
  const routines = Array.isArray(data) ? data : (data.routines || _v2OrgData?.config?.routines || []);
6148
- 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; }
6073
+ if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured</div>'; return; }
6149
6074
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6150
6075
  <thead><tr style="color:var(--text-xs);text-align:left">
6151
6076
  <th style="padding:5px 8px">Name</th><th>Cron</th><th>Last Run</th><th>Status</th>
@@ -6153,7 +6078,7 @@ async function v2RenderOrgRoutines() {
6153
6078
  routines.map(r => `<tr style="border-top:1px solid var(--border)">
6154
6079
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(r.name || '—')}</td>
6155
6080
  <td style="padding:6px 8px;font-family:var(--mono);color:var(--text-lo)">${esc(r.cron || r.schedule || '—')}</td>
6156
- <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>
6081
+ <td style="padding:6px 8px;color:var(--text-lo)">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
6157
6082
  <td style="padding:6px 8px"><span class="ss-pill ${r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || '—')}</span></td>
6158
6083
  </tr>`).join('') + '</tbody></table>';
6159
6084
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6168,7 +6093,7 @@ async function v2RenderOrgMyIssues() {
6168
6093
  const _enc = encodeURIComponent(_v2SelOrg);
6169
6094
  const data = await fetch(`/api/org/${_enc}/my-issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6170
6095
  const issues = Array.isArray(data) ? data : (data.issues || []);
6171
- 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; }
6096
+ if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you</div>'; return; }
6172
6097
  const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
6173
6098
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6174
6099
  <thead><tr style="color:var(--text-xs);text-align:left">
@@ -6178,9 +6103,9 @@ async function v2RenderOrgMyIssues() {
6178
6103
  const cls = i.status === 'done' ? 'on' : i.status === 'blocked' ? 'warn' : '';
6179
6104
  return `<tr style="border-top:1px solid var(--border)">
6180
6105
  <td style="padding:5px 4px">${PRIORITY[i.priority] || '·'}</td>
6181
- <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>
6106
+ <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>
6182
6107
  <td style="padding:5px 8px"><span class="ss-pill ${cls}">${esc(i.status || 'open')}</span></td>
6183
- <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>
6108
+ <td style="padding:5px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(i.updated_at || i.ts)}</td>
6184
6109
  </tr>`;
6185
6110
  }).join('') + '</tbody></table>';
6186
6111
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6209,21 +6134,19 @@ function v2RenderOrgBudgets() {
6209
6134
  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>`;
6210
6135
  }
6211
6136
  if (agents.length) {
6212
- const shownAgents = agents.slice(0, 20);
6213
6137
  html += '<div class="m-group-title" style="margin-bottom:6px">Per Agent</div>' +
6214
6138
  `<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>` +
6215
- shownAgents.map(a => {
6139
+ agents.slice(0, 20).map(a => {
6216
6140
  const over = a.budgetLimit && ((a.tokensIn || 0) + (a.tokensOut || 0)) > a.budgetLimit;
6217
6141
  return `<tr style="border-top:1px solid var(--border)${over ? ';color:var(--red)' : ''}">
6218
- <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>
6142
+ <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>
6219
6143
  <td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensIn || 0).toLocaleString()}</td>
6220
6144
  <td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensOut || 0).toLocaleString()}</td>
6221
6145
  <td style="padding:4px 8px;color:var(--accent)">$${Number(a.cost || 0).toFixed(4)}${over ? ' ⚠' : ''}</td>
6222
6146
  </tr>`;
6223
- }).join('') + '</tbody></table>' +
6224
- (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>` : '');
6147
+ }).join('') + '</tbody></table>';
6225
6148
  }
6226
- 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>';
6149
+ el.innerHTML = html || '<div class="empty">No budget data</div>';
6227
6150
  }
6228
6151
 
6229
6152
  // ── PLUGINS ────────────────────────────────────────────────
@@ -6235,14 +6158,14 @@ async function v2RenderOrgPlugins() {
6235
6158
  const _enc = encodeURIComponent(_v2SelOrg);
6236
6159
  const data = await fetch(`/api/org/${_enc}/plugins${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6237
6160
  const plugins = Array.isArray(data) ? data : (data.plugins || []);
6238
- 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; }
6161
+ if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
6239
6162
  el.innerHTML = `<div class="proj-grid">` +
6240
6163
  plugins.map(p => {
6241
6164
  const status = p.status || 'installed';
6242
6165
  const col = status === 'error' ? 'var(--red)' : status === 'installed' ? 'var(--accent)' : 'var(--text-lo)';
6243
6166
  return `<div class="proj-card">
6244
- <div class="proj-card-name" title="${esc(p.name || '')}">${esc(p.name || '—')}</div>
6245
- <div class="proj-card-path" title="${esc(p.description || '')}">${esc((p.description || '').slice(0, 80))}${(p.description || '').length > 80 ? '…' : ''}</div>
6167
+ <div class="proj-card-name">${esc(p.name || '—')}</div>
6168
+ <div class="proj-card-path">${esc((p.description || '').slice(0, 80))}</div>
6246
6169
  <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>
6247
6170
  </div>`;
6248
6171
  }).join('') + `</div>`;
@@ -6265,7 +6188,7 @@ function v2RenderOrgCharts() {
6265
6188
  events: 0, errors: 0,
6266
6189
  }));
6267
6190
  activity.forEach(e => {
6268
- const _et = e.ts || e.timestamp || e.created_at || 0; const ts = typeof _et === 'number' ? _et : Number(_et) || new Date(_et).getTime() || 0;
6191
+ const ts = new Date(e.ts || e.timestamp || e.created_at || 0).getTime();
6269
6192
  const idx = buckets.findIndex((b, i) =>
6270
6193
  ts >= b.ts && (i === 13 || ts < buckets[i + 1].ts));
6271
6194
  if (idx >= 0) {
@@ -6290,7 +6213,7 @@ function v2RenderOrgCharts() {
6290
6213
  // Per-agent run bars
6291
6214
  const agentRuns = agents.slice(0, 10).map(a => {
6292
6215
  const runs = activity.filter(e => e.agentId === a.id || e.agent === a.id).length;
6293
- return { name: (a.type || a.title || a.id || '—').toString(), runs };
6216
+ return { name: (a.type || a.title || a.id || '—').toString().slice(0, 20), runs };
6294
6217
  }).filter(a => a.runs > 0).sort((x, y) => y.runs - x.runs);
6295
6218
  const maxRuns = Math.max(...agentRuns.map(a => a.runs), 1);
6296
6219
 
@@ -6310,7 +6233,7 @@ function v2RenderOrgCharts() {
6310
6233
  <div style="display:flex;gap:2px;align-items:flex-end;padding-bottom:28px;margin-bottom:16px;overflow-x:auto">${heatmap}</div>
6311
6234
  ${agentRuns.length ? `<div class="m-group-title" style="margin-bottom:6px">Per-Agent Runs</div>
6312
6235
  ${agentRuns.map(a => `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">
6313
- <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>
6236
+ <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>
6314
6237
  <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>
6315
6238
  <div style="width:30px;text-align:right;color:var(--text-lo);font-family:var(--mono);font-size:11px">${a.runs}</div>
6316
6239
  </div>`).join('')}` : ''}
@@ -6326,10 +6249,10 @@ async function v2RenderOrgProjects() {
6326
6249
  const _enc = encodeURIComponent(_v2SelOrg);
6327
6250
  const data = await fetch(`/api/org/${_enc}/projects${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6328
6251
  const projects = Array.isArray(data) ? data : (data.projects || []);
6329
- 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; }
6252
+ if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured</div>'; return; }
6330
6253
  el.innerHTML = `<div class="proj-grid">${projects.map(p => `<div class="proj-card">
6331
- <div class="proj-card-name" title="${esc(p.name || p.title || '')}">${esc(p.name || p.title || '—')}</div>
6332
- <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>
6254
+ <div class="proj-card-name">${esc(p.name || p.title || '—')}</div>
6255
+ <div class="proj-card-path">${esc((p.description || p.path || '').slice(0,80))}</div>
6333
6256
  <div class="proj-card-stats" style="margin-top:8px">
6334
6257
  ${p.status ? `<span class="ss-pill ${p.status==='active'?'on':''}">${esc(p.status)}</span>` : ''}
6335
6258
  </div>
@@ -6342,7 +6265,7 @@ async function v2RenderOrgSkills() {
6342
6265
  const el = document.getElementById('odt-skills');
6343
6266
  if (!el || !_v2OrgData) return;
6344
6267
  const roles = Array.isArray(_v2OrgData.roles) ? _v2OrgData.roles : [];
6345
- 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; }
6268
+ if (!roles.length) { el.innerHTML = '<div class="empty">No roles defined</div>'; return; }
6346
6269
  el.innerHTML = '<div class="empty">Loading skills…</div>';
6347
6270
  const org = _v2SelOrg, dirq = DIR ? '?dir=' + encodeURIComponent(DIR) : '';
6348
6271
  // Enrich each role with expertise + task types from its agent definition (same source as the detail drawer)
@@ -6381,7 +6304,7 @@ async function v2RenderOrgWorkspaces() {
6381
6304
  const _enc = encodeURIComponent(_v2SelOrg);
6382
6305
  const data = await fetch(`/api/org/${_enc}/workspaces${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6383
6306
  const ws = Array.isArray(data) ? data : (data.workspaces || []);
6384
- 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; }
6307
+ if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured</div>'; return; }
6385
6308
  el.innerHTML = ws.map(w => `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
6386
6309
  <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
6387
6310
  <span style="font-size:13px;color:var(--text-hi);font-weight:500">${esc(w.name || w.id || '—')}</span>
@@ -6402,16 +6325,16 @@ async function v2RenderOrgInvites() {
6402
6325
  const _enc = encodeURIComponent(_v2SelOrg);
6403
6326
  const data = await fetch(`/api/org/${_enc}/invites${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6404
6327
  const invites = Array.isArray(data) ? data : (data.invites || []);
6405
- 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; }
6328
+ if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens</div>'; return; }
6406
6329
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6407
6330
  <thead><tr style="color:var(--text-xs);text-align:left">
6408
6331
  <th style="padding:5px 8px">Token</th><th>Role</th><th>Expires</th><th>Created</th>
6409
6332
  </tr></thead>
6410
6333
  <tbody>${invites.map(i => `<tr style="border-top:1px solid var(--border)">
6411
- <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>
6334
+ <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>
6412
6335
  <td style="padding:5px 8px"><span class="ss-pill">${esc(i.role||'viewer')}</span></td>
6413
- <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>
6414
- <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>
6336
+ <td style="padding:5px 8px;color:var(--text-lo)">${i.expiresAt ? relTime(i.expiresAt) : '—'}</td>
6337
+ <td style="padding:5px 8px;color:var(--text-lo)">${i.createdAt ? relTime(i.createdAt) : '—'}</td>
6415
6338
  </tr>`).join('')}</tbody>
6416
6339
  </table>`;
6417
6340
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6422,17 +6345,17 @@ function v2RenderOrgAgentsFull() {
6422
6345
  const el = document.getElementById('odt-agents-full');
6423
6346
  if (!el || !_v2OrgData) return;
6424
6347
  const agents = _v2OrgData._agents || [];
6425
- 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; }
6348
+ if (!agents.length) { el.innerHTML = '<div class="empty">No agents found</div>'; return; }
6426
6349
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6427
6350
  <thead><tr style="color:var(--text-xs);text-align:left">
6428
6351
  <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>
6429
6352
  </tr></thead>
6430
6353
  <tbody>${agents.map(a => `<tr style="border-top:1px solid var(--border)">
6431
- <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>
6354
+ <td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px">${esc((a.id||'—').toString().slice(0,12))}</td>
6432
6355
  <td style="padding:5px 8px;color:var(--text-hi)">${esc(a.type||a.title||'—')}</td>
6433
6356
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapter||'—')}</td>
6434
6357
  <td style="padding:5px 8px"><span class="ss-pill ${(a.status==='running'||a.running)?'on':''}">${esc(a.status||'idle')}</span></td>
6435
- <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>
6358
+ <td style="padding:5px 8px;color:var(--text-lo)">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
6436
6359
  <td style="padding:5px 8px;color:var(--text-lo)">${a.tokensIn != null ? Number(a.tokensIn).toLocaleString() : '—'}</td>
6437
6360
  <td style="padding:5px 8px;color:var(--text-lo)">${a.tokensOut != null ? Number(a.tokensOut).toLocaleString() : '—'}</td>
6438
6361
  </tr>`).join('')}</tbody>
@@ -6448,7 +6371,7 @@ async function v2RenderOrgEnvironments() {
6448
6371
  const _enc = encodeURIComponent(_v2SelOrg);
6449
6372
  const data = await fetch(`/api/org/${_enc}/environments${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6450
6373
  const envs = Array.isArray(data) ? data : (data.environments || []);
6451
- 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; }
6374
+ if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured</div>'; return; }
6452
6375
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6453
6376
  <thead><tr style="color:var(--text-xs);text-align:left">
6454
6377
  <th style="padding:5px 8px">Name</th><th>Driver</th><th>Host</th><th>Status</th>
@@ -6468,7 +6391,7 @@ function v2RenderOrgAccess() {
6468
6391
  const el = document.getElementById('odt-access');
6469
6392
  if (!el || !_v2OrgData) return;
6470
6393
  const members = _v2OrgData._members || [];
6471
- 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; }
6394
+ if (!members.length) { el.innerHTML = '<div class="empty">No members found</div>'; return; }
6472
6395
  const TIER = { owner: 0, admin: 1, operator: 2, viewer: 3 };
6473
6396
  const byRole = {};
6474
6397
  members.forEach(m => { const r = m.role || 'viewer'; (byRole[r] || (byRole[r] = [])).push(m); });
@@ -6490,7 +6413,7 @@ function v2RenderOrgIssuesFull() {
6490
6413
  const el = document.getElementById('odt-issues-full');
6491
6414
  if (!el || !_v2OrgData) return;
6492
6415
  const issues = _v2OrgData._issues || [];
6493
- 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; }
6416
+ if (!issues.length) { el.innerHTML = '<div class="empty">No issues found</div>'; return; }
6494
6417
  const PRIORITY = { urgent:'🔴', high:'🟠', medium:'🟡', low:'🟢' };
6495
6418
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6496
6419
  <thead><tr style="color:var(--text-xs);text-align:left">
@@ -6498,10 +6421,10 @@ function v2RenderOrgIssuesFull() {
6498
6421
  </tr></thead>
6499
6422
  <tbody>${issues.slice(0,100).map(i => `<tr style="border-top:1px solid var(--border)">
6500
6423
  <td style="padding:5px 4px">${PRIORITY[i.priority]||'·'}</td>
6501
- <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>
6424
+ <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>
6502
6425
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(i.assignee||'—')}</td>
6503
6426
  <td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'?'on':i.status==='blocked'?'warn':''}">${esc(i.status||'open')}</span></td>
6504
- <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>
6427
+ <td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px">${relTime(i.updated_at||i.ts)}</td>
6505
6428
  </tr>`).join('')}</tbody>
6506
6429
  </table>`;
6507
6430
  }
@@ -6515,7 +6438,7 @@ async function v2RenderOrgJoinRequests() {
6515
6438
  const _enc = encodeURIComponent(_v2SelOrg);
6516
6439
  const data = await fetch(`/api/org/${_enc}/join-requests${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6517
6440
  const reqs = Array.isArray(data) ? data : (data.joinRequests || data.join_requests || []);
6518
- 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; }
6441
+ if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests</div>'; return; }
6519
6442
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6520
6443
  <thead><tr style="color:var(--text-xs);text-align:left">
6521
6444
  <th style="padding:5px 8px">Requester</th><th>Type</th><th>Status</th><th>Created</th>
@@ -6524,7 +6447,7 @@ async function v2RenderOrgJoinRequests() {
6524
6447
  <td style="padding:5px 8px;color:var(--text-hi)">${esc(r.name||r.username||r.id||'—')}</td>
6525
6448
  <td style="padding:5px 8px;color:var(--text-lo)">${r.type==='agent'?'🤖 agent':'👤 human'}</td>
6526
6449
  <td style="padding:5px 8px"><span class="ss-pill ${r.status==='approved'?'on':r.status==='rejected'?'warn':''}">${esc(r.status||'pending')}</span></td>
6527
- <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>
6450
+ <td style="padding:5px 8px;color:var(--text-lo)">${r.createdAt ? relTime(r.createdAt) : '—'}</td>
6528
6451
  </tr>`).join('')}</tbody>
6529
6452
  </table>`;
6530
6453
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6539,17 +6462,17 @@ async function v2RenderOrgThreads() {
6539
6462
  const _enc = encodeURIComponent(_v2SelOrg);
6540
6463
  const data = await fetch(`/api/org/${_enc}/threads${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6541
6464
  const threads = Array.isArray(data) ? data : (data.threads || []);
6542
- 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; }
6465
+ if (!threads.length) { el.innerHTML = '<div class="empty">No threads found</div>'; return; }
6543
6466
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6544
6467
  <thead><tr style="color:var(--text-xs);text-align:left">
6545
6468
  <th style="padding:5px 8px">Subject</th><th>Author</th><th>Messages</th><th>Issue</th><th>Created</th>
6546
6469
  </tr></thead>
6547
6470
  <tbody>${threads.map(t => `<tr style="border-top:1px solid var(--border)">
6548
- <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>
6471
+ <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>
6549
6472
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(t.author||t.createdBy||'—')}</td>
6550
6473
  <td style="padding:5px 8px;color:var(--text-lo);text-align:center">${t.messageCount ?? t.messages ?? '—'}</td>
6551
- <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>
6552
- <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>
6474
+ <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>
6475
+ <td style="padding:5px 8px;color:var(--text-lo)">${t.createdAt ? relTime(t.createdAt) : '—'}</td>
6553
6476
  </tr>`).join('')}</tbody>
6554
6477
  </table>`;
6555
6478
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6559,103 +6482,55 @@ async function v2RenderOrgThreads() {
6559
6482
  window.v2StopOrg = async function() {
6560
6483
  if (!_v2SelOrg) return;
6561
6484
  const stopped = _v2SelOrg;
6562
- try {
6563
- await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'});
6564
- showToast('Stopped', `Org "${stopped}" stopped`, 'ok');
6565
- } catch(e) { showToast('Error', e.message, 'err'); }
6485
+ try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'}); } catch(_) {}
6566
6486
  setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
6567
6487
  };
6568
6488
 
6569
- // ── Copy org dialog ────────────────────────────────────────────────────────────
6570
- window.v2ShowCopyOrgDialog = async function() {
6571
- if (!_v2SelOrg) return;
6572
- const dialog = document.getElementById('org-copy-dialog');
6573
- const feedback = document.getElementById('org-copy-feedback');
6574
- const select = document.getElementById('org-copy-dest-select');
6575
- const input = document.getElementById('org-copy-dest-input');
6576
- if (!dialog) return;
6577
- feedback.textContent = '';
6578
- input.value = '';
6579
- select.innerHTML = '<option value="">Loading…</option>';
6580
- dialog.style.display = 'flex';
6581
- // Populate project list
6582
- try {
6583
- const data = await apiFetch('/api/projects');
6584
- const projects = (data.projects || []).filter(p => p.path && p.path !== DIR);
6585
- select.innerHTML = '<option value="">— select a project —</option>' +
6586
- projects.map(p => `<option value="${esc(p.path)}">${esc(p.name)} (${esc(p.path)})</option>`).join('') +
6587
- '<option value="__custom__">Custom path…</option>';
6588
- } catch(e) {
6589
- select.innerHTML = '<option value="__custom__">Enter path manually</option>';
6590
- }
6489
+ window.v2ShowCopyOrgDialog = function() {
6490
+ const d = document.getElementById('org-copy-dialog');
6491
+ if (d) { d.style.display = 'flex'; document.getElementById('org-copy-dest')?.focus(); }
6591
6492
  };
6592
-
6593
- window.v2HideCopyOrgDialog = function() {
6594
- const dialog = document.getElementById('org-copy-dialog');
6595
- if (dialog) dialog.style.display = 'none';
6596
- };
6597
-
6598
6493
  window.v2DoCopyOrg = async function() {
6494
+ const dest = document.getElementById('org-copy-dest')?.value.trim();
6495
+ if (!dest) { showToast('Required', 'Enter destination path', 'warn'); return; }
6599
6496
  if (!_v2SelOrg) return;
6600
- const select = document.getElementById('org-copy-dest-select');
6601
- const input = document.getElementById('org-copy-dest-input');
6602
- const feedback = document.getElementById('org-copy-feedback');
6603
- const btn = document.getElementById('org-copy-confirm-btn');
6604
- const destination = (input.value.trim()) || (select.value !== '__custom__' ? select.value : '');
6605
- if (!destination) {
6606
- feedback.textContent = 'Please select or enter a destination.';
6607
- feedback.style.color = 'var(--red, #e55)';
6608
- return;
6609
- }
6610
- btn.disabled = true;
6611
- btn.textContent = 'Copying…';
6612
- feedback.textContent = '';
6613
6497
  try {
6614
- const r = await fetch(`/api/orgs/${encodeURIComponent(_v2SelOrg)}/copy`, {
6615
- method: 'POST',
6616
- headers: { 'Content-Type': 'application/json' },
6617
- body: JSON.stringify({ destination }),
6498
+ const r = await fetch('/api/org/' + enc(_v2SelOrg) + '/copy?dir=' + enc(DIR), {
6499
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
6500
+ body: JSON.stringify({ destination: dest }),
6618
6501
  });
6619
- const json = await r.json();
6620
- if (r.ok && json.ok) {
6621
- feedback.textContent = '✓ Copied successfully to ' + destination;
6622
- feedback.style.color = 'var(--green, #4ade80)';
6623
- setTimeout(v2HideCopyOrgDialog, 1800);
6624
- } else {
6625
- feedback.textContent = 'Error: ' + (json.error || r.status);
6626
- feedback.style.color = 'var(--red, #e55)';
6627
- }
6628
- } catch(e) {
6629
- feedback.textContent = 'Error: ' + e.message;
6630
- feedback.style.color = 'var(--red, #e55)';
6631
- } finally {
6632
- btn.disabled = false;
6633
- btn.textContent = 'Copy';
6634
- }
6502
+ if (!r.ok) throw new Error('HTTP ' + r.status);
6503
+ showToast('Copied', `Org "${_v2SelOrg}" copied to ${dest}`, 'ok');
6504
+ document.getElementById('org-copy-dialog').style.display = 'none';
6505
+ } catch (e) { showToast('Error', e.message, 'err'); }
6635
6506
  };
6636
6507
 
6508
+ function filterLoopList(q) {
6509
+ const query = q.trim().toLowerCase();
6510
+ document.querySelectorAll('#loops-content .loop-row').forEach(row => {
6511
+ const show = !query || (row.textContent || '').toLowerCase().includes(query);
6512
+ row.style.display = show ? '' : 'none';
6513
+ const expand = row.nextElementSibling;
6514
+ if (expand && expand.classList.contains('loop-expand')) expand.style.display = show ? '' : 'none';
6515
+ });
6516
+ }
6517
+
6518
+ function filterOrgList(q) {
6519
+ const query = q.trim().toLowerCase();
6520
+ document.querySelectorAll('#orgs-list-content .org-item').forEach(item => {
6521
+ item.style.display = (!query || (item.textContent || '').toLowerCase().includes(query)) ? '' : 'none';
6522
+ });
6523
+ }
6524
+
6637
6525
  // live SSE for org events
6638
6526
  (function v2OrgSSE() {
6639
6527
  let src;
6640
- let _connectTime = 0;
6641
6528
  function connect() {
6642
6529
  if (src) src.close();
6643
- _connectTime = Date.now();
6644
6530
  src = new EventSource('/api/mastermind-stream');
6645
6531
  src.onmessage = e => {
6646
6532
  try {
6647
6533
  const ev = JSON.parse(e.data);
6648
- if (ev?.type?.startsWith('loop:')) {
6649
- // Skip replayed historical events (server replays last 50 on connect)
6650
- if (ev.ts && ev.ts < _connectTime) return;
6651
- if (currentView === 'loops') renderLoops();
6652
- else viewRendered['loops'] = false; // stale — re-fetch on next switchView
6653
- loadLoopMetrics();
6654
- if (_mmCurrentTab === 'loops' && document.getElementById('mm-overlay')?.classList.contains('open')) {
6655
- mmRenderLoops(document.getElementById('mm-body'));
6656
- }
6657
- return;
6658
- }
6659
6534
  if (!ev?.org || !ev?.type?.startsWith('org:')) return;
6660
6535
  const n = ev.org;
6661
6536
  // Filter by project dir if the event carries one; skip events from other projects.
@@ -6724,12 +6599,11 @@ function buildTimeline(events) {
6724
6599
  // Only tool + user events with timestamps
6725
6600
  const stamped = events.filter(ev => ev.ts && (ev.kind === 'tool' || ev.kind === 'user'));
6726
6601
  if (stamped.length < 2) { tl.innerHTML = ''; return; }
6727
- const tsMs = ev => typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || 0;
6728
- const times = stamped.map(tsMs);
6602
+ const times = stamped.map(ev => new Date(ev.ts).getTime());
6729
6603
  const tMin = Math.min(...times), tMax = Math.max(...times);
6730
6604
  const span = tMax - tMin || 1;
6731
6605
  const segs = stamped.map(ev => {
6732
- const pct = ((tsMs(ev) - tMin) / span * 100).toFixed(2);
6606
+ const pct = ((new Date(ev.ts).getTime() - tMin) / span * 100).toFixed(2);
6733
6607
  const cat = ev.kind === 'user' ? 'user' : (ev.cat || 'other');
6734
6608
  const color = TL_COLORS[cat] || TL_COLORS.other;
6735
6609
  const label = ev.kind === 'user' ? 'user message' : (ev.label || ev.name || cat);
@@ -6824,13 +6698,12 @@ async function copySession() {
6824
6698
  try {
6825
6699
  const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
6826
6700
  const events = data.events || [];
6827
- const _sessTs = sess.lastTs || sess.mtime;
6828
- const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(typeof _sessTs === 'number' ? _sessTs : Number(_sessTs) || _sessTs).toLocaleString()}`, ''];
6701
+ const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`, ''];
6829
6702
  for (const ev of events) {
6830
6703
  if (ev.kind === 'user' && ev.text?.trim()) {
6831
6704
  lines.push('**User:** ' + ev.text.trim().replace(/\n/g, ' '));
6832
6705
  } else if (ev.kind === 'tool') {
6833
- lines.push(`- \`${ev.name || ev.cat}\`${ev.label ? ': ' + ev.label : ''}`);
6706
+ lines.push(`- \`${ev.name || ev.cat}\`: ${ev.label || ''}`);
6834
6707
  }
6835
6708
  }
6836
6709
  await navigator.clipboard.writeText(lines.join('\n'));
@@ -6897,10 +6770,7 @@ function cmdSearch(q) {
6897
6770
  results.innerHTML = sq.length >= 2
6898
6771
  ? '<div class="cmd-empty">Searching sessions…</div>'
6899
6772
  : '<div class="cmd-empty">Type at least 2 chars after &gt; to search all sessions</div>';
6900
- if (sq.length >= 2) {
6901
- clearTimeout(cmdSearch._debounce);
6902
- cmdSearch._debounce = setTimeout(() => searchSessions(sq), 300);
6903
- }
6773
+ if (sq.length >= 2) searchSessions(sq);
6904
6774
  return;
6905
6775
  }
6906
6776
 
@@ -6929,20 +6799,10 @@ function cmdSearch(q) {
6929
6799
 
6930
6800
  // ACTIONS group
6931
6801
  const actionItems = [
6932
- { label: 'Go to Now', action: () => switchView('now') },
6933
- { label: 'Go to Sessions', action: () => switchView('sessions') },
6934
- { label: 'Go to Projects', action: () => switchView('projects') },
6935
- { label: 'Go to Loops', action: () => switchView('loops') },
6936
- { label: 'Go to Tokens', action: () => switchView('tokens') },
6937
- { label: 'Go to Memory', action: () => switchView('memory') },
6938
- { label: 'Go to Orgs', action: () => switchView('orgs') },
6939
- { label: 'Go to Monograph', action: () => switchView('monograph') },
6940
- { label: 'Go to Global Feed',action: () => switchView('global') },
6802
+ { label: 'Open Monograph', action: () => { const l = document.querySelector('.nav-item[data-view="monograph"]'); if (l) l.click(); } },
6941
6803
  { label: 'Refresh Dashboard', action: () => refreshCurrent() },
6942
6804
  { label: 'Toggle Compact View', action: () => toggleDensity() },
6943
- { label: 'Toggle Ambient Mode', action: () => toggleAmbient() },
6944
- { label: 'Open Mastermind', action: () => openMastermind() },
6945
- { label: 'Keyboard Shortcuts', action: () => { closeCmdPalette(); openShortcutHelp(); } },
6805
+ { label: 'Open Loops', action: () => { const l = document.querySelector('.nav-item[data-view="loops"]'); if (l) l.click(); } },
6946
6806
  ].filter(a => !lq || a.label.toLowerCase().includes(lq));
6947
6807
 
6948
6808
  // TABS group — only if org room is open
@@ -6968,8 +6828,8 @@ function cmdSearch(q) {
6968
6828
  html += `<div class="cmd-item" data-ci="${idx}">
6969
6829
  <span class="ci-ico">◫</span>
6970
6830
  <div class="cmd-item-body">
6971
- <div class="ci-title" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
6972
- <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>
6831
+ <div class="ci-title">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
6832
+ <div class="ci-sub">${relTime(s.lastTs || s.mtime)}</div>
6973
6833
  </div>
6974
6834
  </div>`;
6975
6835
  });
@@ -6984,7 +6844,7 @@ function cmdSearch(q) {
6984
6844
  <span class="ci-ico">◈</span>
6985
6845
  <div class="cmd-item-body">
6986
6846
  <div class="ci-title">${esc(d.key || d.namespace || '—')}</div>
6987
- <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>
6847
+ <div class="ci-sub">${esc(String(d.value || d.text || '').slice(0, 60))}</div>
6988
6848
  </div>
6989
6849
  </div>`;
6990
6850
  });
@@ -7030,7 +6890,7 @@ function cmdSearch(q) {
7030
6890
  <span class="ci-ico">✦</span>
7031
6891
  <div class="cmd-item-body">
7032
6892
  <div class="ci-title">${esc(skLabel)}</div>
7033
- <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>
6893
+ <div class="ci-sub">${typeof sk === 'string' ? '' : esc((sk.description || '').slice(0, 60))}</div>
7034
6894
  </div>
7035
6895
  </div>`;
7036
6896
  });
@@ -7097,7 +6957,7 @@ function executeCmdItem() {
7097
6957
  else if (item.type === 'memory') switchView('memory');
7098
6958
  else if (item.type === 'project') switchProject(item.data.path);
7099
6959
  else if (item.type === 'org') { switchView('orgs'); setTimeout(() => v2SelectOrg(item.data.name), 80); }
7100
- else if (item.type === 'skill') { const _skN = typeof item.data === 'string' ? item.data : (item.data.name || item.data.id || ''); _mmSkillFilter = _skN; openMastermind(); mmSwitchTab('skills'); }
6960
+ else if (item.type === 'skill') switchView('memory');
7101
6961
  else if (item.type === 'action') { if (item.data.action) item.data.action(); }
7102
6962
  else if (item.type === 'orgtab') { const btn = document.querySelector(`.odt-btn[data-tab="${item.data.tab}"]`); if (btn) btn.click(); }
7103
6963
  }
@@ -7120,8 +6980,8 @@ async function searchSessions(q) {
7120
6980
  html += `<div class="cmd-item" data-ci="${idx}">
7121
6981
  <span class="ci-ico">◫</span>
7122
6982
  <div class="cmd-item-body">
7123
- <div class="ci-title" title="${esc(r.lastPrompt || r.id || '')}">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
7124
- <div class="ci-sub" title="${esc(snippet)}">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
6983
+ <div class="ci-title">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
6984
+ <div class="ci-sub">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
7125
6985
  </div>
7126
6986
  </div>`;
7127
6987
  });
@@ -7146,22 +7006,21 @@ document.addEventListener('keydown', e => {
7146
7006
  return;
7147
7007
  }
7148
7008
 
7149
- // Escape always closes modals, even when focused inside an input/textarea
7150
- if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); closeBudgetModal(); closeChunkModal(); closeMemModal(); closeReportCard(); closeMastermind(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); return; }
7151
-
7152
7009
  // ignore when typing in inputs
7153
7010
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
7154
7011
  if (document.getElementById('cmd-palette').classList.contains('open')) return;
7012
+
7013
+ if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); closeMastermind(); closeBudgetModal(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
7155
7014
  if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
7156
- if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); return; }
7157
7015
  if (e.key === 'm' || e.key === 'M') { e.preventDefault(); openMastermind(); return; }
7016
+ const _vkeys = { '1':'now','2':'sessions','3':'projects','4':'loops','5':'tokens','6':'memory','7':'orgs','8':'monograph','9':'global' };
7017
+ if (_vkeys[e.key] && !e.ctrlKey && !e.metaKey && !e.altKey) { switchView(_vkeys[e.key]); return; }
7158
7018
  if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
7159
- const _viewKeys = {'1':'now','2':'sessions','3':'projects','4':'loops','5':'tokens','6':'memory','7':'orgs','8':'monograph','9':'global'};
7160
- if (_viewKeys[e.key] && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); switchView(_viewKeys[e.key]); return; }
7161
7019
 
7162
7020
  if (currentView === 'now') {
7163
7021
  if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
7164
7022
  if (e.key === 'g' || e.key === 'G') { e.preventDefault(); goLive(); }
7023
+ if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); }
7165
7024
 
7166
7025
  if (e.key === 'j' || e.key === 'k') {
7167
7026
  e.preventDefault();
@@ -7172,11 +7031,9 @@ document.addEventListener('keydown', e => {
7172
7031
  else cur = cur < 0 ? 0 : Math.max(cur - 1, 0);
7173
7032
  entries.forEach((el, i) => el.classList.toggle('selected', i === cur));
7174
7033
  entries[cur].scrollIntoView({ block: 'nearest' });
7175
- try {
7176
- selectedEntryId = entries[cur].dataset.ev
7177
- ? (JSON.parse(entries[cur].dataset.ev).id || '')
7178
- : '';
7179
- } catch { selectedEntryId = ''; }
7034
+ selectedEntryId = entries[cur].dataset.ev
7035
+ ? (JSON.parse(entries[cur].dataset.ev).id || '')
7036
+ : '';
7180
7037
  }
7181
7038
 
7182
7039
  if (e.key === 'Enter') {
@@ -7231,22 +7088,6 @@ function filterSessions(q) {
7231
7088
  });
7232
7089
  const countEl = document.getElementById('sess-filter-count');
7233
7090
  if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
7234
- // zero-results empty state
7235
- const noRes = document.getElementById('sess-filter-noresult');
7236
- if (lq && visible === 0 && rows.length > 0) {
7237
- if (!noRes) {
7238
- const el = document.createElement('div');
7239
- el.id = 'sess-filter-noresult';
7240
- el.className = 'empty';
7241
- el.style.cssText = 'padding:24px 0;font-size:13px';
7242
- el.textContent = 'No sessions match "' + q.slice(0, 40) + '"';
7243
- document.getElementById('sess-content').appendChild(el);
7244
- } else {
7245
- noRes.textContent = 'No sessions match "' + q.slice(0, 40) + '"';
7246
- }
7247
- } else if (noRes) {
7248
- noRes.remove();
7249
- }
7250
7091
  }
7251
7092
 
7252
7093
  // ── feature 32: keyboard shortcut help modal ──────────────
@@ -7273,7 +7114,7 @@ function updateCurrentActivity(events) {
7273
7114
  } else if (name) {
7274
7115
  activity = name;
7275
7116
  }
7276
- if (activity) { el.textContent = '⤷ ' + activity; el.title = recent.label || name || ''; el.classList.add('loaded'); }
7117
+ if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
7277
7118
  else { el.textContent = ''; el.classList.remove('loaded'); }
7278
7119
  }
7279
7120
 
@@ -7297,8 +7138,7 @@ function buildPatterns() {
7297
7138
  if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
7298
7139
  }
7299
7140
  }
7300
- const allTerms = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]);
7301
- const sorted = allTerms.slice(0, 20);
7141
+ const sorted = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20);
7302
7142
  if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
7303
7143
  const maxCount = sorted[0][1];
7304
7144
  const rows = sorted.map(([word, count], i) => {
@@ -7311,8 +7151,7 @@ function buildPatterns() {
7311
7151
  }).join('');
7312
7152
  el.innerHTML = `<table class="lb-table"><thead><tr>
7313
7153
  <th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
7314
- </tr></thead><tbody>${rows}</tbody></table>` +
7315
- (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>` : '');
7154
+ </tr></thead><tbody>${rows}</tbody></table>`;
7316
7155
  }
7317
7156
 
7318
7157
  // ── feature 35: session streak tracker ────────────────────
@@ -7320,7 +7159,7 @@ function calcStreak() {
7320
7159
  const dates = new Set(allSessions.map(s => {
7321
7160
  const t = s.firstTs || s.mtime;
7322
7161
  if (!t) return null;
7323
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString();
7162
+ return new Date(typeof t === 'number' ? t : t).toDateString();
7324
7163
  }).filter(Boolean));
7325
7164
  let streak = 0;
7326
7165
  const today = new Date();
@@ -7338,16 +7177,14 @@ function calcStreak() {
7338
7177
 
7339
7178
  // ── feature 25: notification toasts ──────────────────────
7340
7179
  let _toastLastBudgetKey = '';
7341
- let _toastLastKey = ''; let _toastLastTs = 0;
7342
7180
  function showToast(title, msg, type = 'info', duration = 5000) {
7181
+ const key = title + '|' + msg;
7182
+ if (showToast._last === key && Date.now() - (showToast._lastTs || 0) < 2000) return;
7183
+ showToast._last = key; showToast._lastTs = Date.now();
7184
+ const existing = document.querySelectorAll('.toast');
7185
+ if (existing.length >= 5) existing[0].remove();
7343
7186
  const rack = document.getElementById('toast-rack');
7344
7187
  if (!rack) return;
7345
- // Dedup: skip identical toast within 2s
7346
- const key = type + '|' + title + '|' + msg;
7347
- if (key === _toastLastKey && Date.now() - _toastLastTs < 2000) return;
7348
- _toastLastKey = key; _toastLastTs = Date.now();
7349
- // Cap at 5 visible toasts — evict oldest
7350
- while (rack.children.length >= 5) rack.firstChild.remove();
7351
7188
  const icoMap = { warn: '⚑', err: '⚠', ok: '✓', info: '◉' };
7352
7189
  const div = document.createElement('div');
7353
7190
  div.className = 'toast t-' + type;
@@ -7356,7 +7193,7 @@ function showToast(title, msg, type = 'info', duration = 5000) {
7356
7193
  <div class="toast-title">${esc(title)}</div>
7357
7194
  <div class="toast-msg">${esc(msg)}</div>
7358
7195
  </div>
7359
- <button class="toast-close" onclick="this.closest('.toast').remove()" title="Dismiss">✕</button>`;
7196
+ <button class="toast-close" onclick="this.closest('.toast').remove()">✕</button>`;
7360
7197
  rack.appendChild(div);
7361
7198
  if (duration > 0) setTimeout(() => { try { div.remove(); } catch {} }, duration);
7362
7199
  }
@@ -7427,11 +7264,7 @@ function buildSessionHeatmap(sessions) {
7427
7264
  if (idx >= 0 && idx < DAYS) buckets[idx].count++;
7428
7265
  }
7429
7266
  const max = Math.max(...buckets.map(b => b.count), 1);
7430
- // pad so column 0 starts on Monday
7431
- const firstDowShm = new Date(now - (DAYS - 1) * DAY).getDay(); // 0=Sun
7432
- const shmOffset = firstDowShm === 0 ? 6 : firstDowShm - 1;
7433
- const padShm = Array.from({ length: shmOffset }, () => '<div class="shm-cell" style="opacity:0;pointer-events:none"></div>');
7434
- el.innerHTML = padShm.join('') + buckets.map(b => {
7267
+ el.innerHTML = buckets.map(b => {
7435
7268
  const level = b.count === 0 ? 0 : Math.min(4, Math.ceil(b.count / max * 4));
7436
7269
  const isActive = b.date === heatmapDateFilter;
7437
7270
  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>`;
@@ -7525,8 +7358,7 @@ function bulkExport() {
7525
7358
  if (!toExport.length) return;
7526
7359
  const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'Files Touched'];
7527
7360
  const rows = toExport.map(s => {
7528
- const _ts0 = s.firstTs || s.mtime || 0;
7529
- const dt = new Date(typeof _ts0 === 'number' ? _ts0 : Number(_ts0) || _ts0).toISOString().slice(0, 19).replace('T', ' ');
7361
+ const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
7530
7362
  const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
7531
7363
  const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
7532
7364
  const prompt = (s.lastPrompt || '').replace(/"/g, '""');
@@ -7580,8 +7412,7 @@ function exportSessionsCSV() {
7580
7412
  if (!allSessions.length) { showToast('No data', 'No sessions loaded yet', 'warn'); return; }
7581
7413
  const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'User Messages', 'Cache Hit %', 'Input Tokens'];
7582
7414
  const rows = allSessions.map(s => {
7583
- const _ts1 = s.firstTs || s.mtime || 0;
7584
- const dt = new Date(typeof _ts1 === 'number' ? _ts1 : Number(_ts1) || _ts1).toISOString().slice(0, 19).replace('T', ' ');
7415
+ const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
7585
7416
  const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
7586
7417
  const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
7587
7418
  const cachePct = s.totalInputTokens > 0 ? Math.round((s.cacheReadTokens || 0) / s.totalInputTokens * 100) : '';
@@ -7614,12 +7445,11 @@ async function loadToolRank() {
7614
7445
  const data = await apiFetch('/api/tool-ranking?dir=' + enc(DIR));
7615
7446
  if (!data.tools?.length) { el.innerHTML = '<div class="loading-txt">No tool usage data</div>'; return; }
7616
7447
  const maxCount = data.tools[0].count;
7617
- const shownTools = data.tools.slice(0, 15);
7618
- const rows = shownTools.map((t, i) => {
7448
+ const rows = data.tools.slice(0, 15).map((t, i) => {
7619
7449
  const barW = Math.round((t.count / maxCount) * 100);
7620
7450
  const errRate = t.errors > 0 ? ((t.errors / t.count) * 100).toFixed(0) + '%' : '—';
7621
7451
  return `<tr><td class="lb-rank">${i + 1}</td>
7622
- <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>
7452
+ <td style="font-size:12px;color:var(--text-mid);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.tool)}</td>
7623
7453
  <td style="width:80px;padding:4px 6px">
7624
7454
  <div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
7625
7455
  <div style="height:100%;width:${barW}%;background:oklch(65% 0.15 200);border-radius:2px"></div>
@@ -7631,8 +7461,7 @@ async function loadToolRank() {
7631
7461
  }).join('');
7632
7462
  el.innerHTML = `<table class="lb-table"><thead><tr>
7633
7463
  <th class="lb-rank">#</th><th>Tool</th><th></th><th class="lb-cost">Calls</th><th class="lb-dur">Error%</th>
7634
- </tr></thead><tbody>${rows}</tbody></table>` +
7635
- (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>` : '');
7464
+ </tr></thead><tbody>${rows}</tbody></table>`;
7636
7465
  } catch (err) {
7637
7466
  el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
7638
7467
  }
@@ -7654,14 +7483,13 @@ async function loadProjCosts() {
7654
7483
  const data = await apiFetch('/api/project-costs');
7655
7484
  if (!data.projects?.length) { el.innerHTML = '<div class="loading-txt">No cost data across projects</div>'; return; }
7656
7485
  const maxCost = data.projects[0].cost;
7657
- const shownProjects = data.projects.slice(0, 10);
7658
- const rows = shownProjects.map((p, i) => {
7486
+ const rows = data.projects.slice(0, 10).map((p, i) => {
7659
7487
  const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
7660
7488
  const name = p.path.split('/').filter(Boolean).pop() || p.path;
7661
7489
  return `<tr onclick="switchProject('${esc(p.path)}')" style="cursor:pointer" title="${esc(p.path)}">
7662
7490
  <td class="lb-rank">${i + 1}</td>
7663
7491
  <td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
7664
- <td class="lb-cost">$${(p.cost || 0).toFixed(2)}</td>
7492
+ <td class="lb-cost">$${p.cost.toFixed(2)}</td>
7665
7493
  <td style="width:80px;padding:4px 6px">
7666
7494
  <div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
7667
7495
  <div style="height:100%;width:${barW}%;background:oklch(72% 0.18 75 / 0.7);border-radius:2px"></div>
@@ -7672,8 +7500,7 @@ async function loadProjCosts() {
7672
7500
  }).join('');
7673
7501
  el.innerHTML = `<table class="lb-table"><thead><tr>
7674
7502
  <th class="lb-rank">#</th><th>Project</th><th class="lb-cost">Cost</th><th></th><th class="lb-dur">Sessions</th>
7675
- </tr></thead><tbody>${rows}</tbody></table>` +
7676
- (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>` : '');
7503
+ </tr></thead><tbody>${rows}</tbody></table>`;
7677
7504
  } catch (err) {
7678
7505
  el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
7679
7506
  }
@@ -7701,7 +7528,7 @@ function buildGanttTimeline() {
7701
7528
  }
7702
7529
  for (const s of allSessions) {
7703
7530
  const t = s.firstTs || s.mtime; if (!t) continue;
7704
- const d = new Date(typeof t === 'number' ? t : Number(t) || t); d.setHours(0,0,0,0);
7531
+ const d = new Date(typeof t === 'number' ? t : t); d.setHours(0,0,0,0);
7705
7532
  const key = d.toDateString();
7706
7533
  if (key in days) days[key].push(s);
7707
7534
  }
@@ -7719,11 +7546,11 @@ function buildGanttTimeline() {
7719
7546
  // Sort sessions by start time
7720
7547
  const sorted = [...sessions].sort((a, b) => {
7721
7548
  const ta = a.firstTs || a.mtime || 0; const tb = b.firstTs || b.mtime || 0;
7722
- return (typeof ta === 'number' ? ta : Number(ta) || new Date(ta).getTime() || 0) - (typeof tb === 'number' ? tb : Number(tb) || new Date(tb).getTime() || 0);
7549
+ return (typeof ta === 'number' ? ta : new Date(ta).getTime()) - (typeof tb === 'number' ? tb : new Date(tb).getTime());
7723
7550
  });
7724
7551
  for (let i = 0; i < sorted.length; i++) {
7725
7552
  const s = sorted[i];
7726
- const _sT = s.firstTs || s.mtime; const startTs = typeof _sT === 'number' ? _sT : Number(_sT) || new Date(_sT).getTime() || 0;
7553
+ const startTs = typeof (s.firstTs || s.mtime) === 'number' ? (s.firstTs || s.mtime) : new Date(s.firstTs || s.mtime).getTime();
7727
7554
  const dayStart = new Date(dateStr).getTime();
7728
7555
  const startPct = Math.min(95, ((startTs - dayStart) / DAY) * 100);
7729
7556
  const durPct = s.totalDurationMs ? Math.max(0.5, Math.min(15, (s.totalDurationMs / DAY) * 100)) : 1;
@@ -7750,7 +7577,7 @@ function showReportCard() {
7750
7577
 
7751
7578
  const todaySess = allSessions.filter(s => {
7752
7579
  const t = s.firstTs || s.mtime; if (!t) return false;
7753
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString() === todayStr;
7580
+ return new Date(typeof t === 'number' ? t : t).toDateString() === todayStr;
7754
7581
  });
7755
7582
  const weekSess = allSessions.filter(s => {
7756
7583
  const t = s.firstTs || s.mtime; if (!t) return false;
@@ -7900,7 +7727,7 @@ function showCostExplainer(sessId, event) {
7900
7727
  .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('');
7901
7728
  drawer.innerHTML = `<div class="err-drawer-head" style="color:oklch(70% 0.18 80)">
7902
7729
  <span>Cost anomaly — $${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
7903
- <button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''" title="Close">✕</button>
7730
+ <button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''">✕</button>
7904
7731
  </div>
7905
7732
  <div class="err-drawer-body">
7906
7733
  <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>
@@ -7969,7 +7796,7 @@ function buildHourlyHeatmap() {
7969
7796
  const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
7970
7797
  for (const s of allSessions) {
7971
7798
  const t = s.firstTs || s.mtime; if (!t) continue;
7972
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
7799
+ const d = new Date(typeof t === 'number' ? t : t);
7973
7800
  const dow = d.getDay(); // 0=Sun
7974
7801
  const hour = d.getHours();
7975
7802
  grid[dow][hour]++;
@@ -8000,7 +7827,7 @@ function buildHourlyHeatmap() {
8000
7827
 
8001
7828
  // ── feature 50: custom tag editor ─────────────────────────
8002
7829
  const _customTagsKey = 'mm-custom-tags';
8003
- let _customTagsMap = new Map(Object.entries((function(){ try { return JSON.parse(localStorage.getItem(_customTagsKey) || '{}'); } catch { return {}; } })()));
7830
+ let _customTagsMap; try { _customTagsMap = new Map(Object.entries(JSON.parse(localStorage.getItem(_customTagsKey) || '{}'))); } catch { _customTagsMap = new Map(); }
8004
7831
 
8005
7832
  function getCustomTags(sessId) {
8006
7833
  return _customTagsMap.get(sessId) || [];
@@ -8020,12 +7847,10 @@ function addCustomTag(sessId, tag, event) {
8020
7847
  if (!tags.includes(t)) { tags.push(t); saveCustomTags(sessId, tags); }
8021
7848
  const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
8022
7849
  if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
8023
- // rebuild tag filter bar full re-render if bar was absent (e.g. just transitioned from 0 tags)
7850
+ // rebuild tag filter bar in the DOM if it exists
8024
7851
  initTags();
8025
7852
  const tfBar = document.querySelector('.tag-filter-bar');
8026
- const newBarHtml = buildTagFilterBar(allSessions);
8027
- if (tfBar) tfBar.outerHTML = newBarHtml;
8028
- else if (newBarHtml) renderSessions();
7853
+ if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
8029
7854
  }
8030
7855
 
8031
7856
  function removeCustomTag(sessId, tag, event) {
@@ -8034,9 +7859,10 @@ function removeCustomTag(sessId, tag, event) {
8034
7859
  saveCustomTags(sessId, tags);
8035
7860
  const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
8036
7861
  if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
8037
- initTags();
8038
7862
  const tfBar = document.querySelector('.tag-filter-bar');
8039
- if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
7863
+ const newBarHtml = buildTagFilterBar(allSessions);
7864
+ if (tfBar) tfBar.outerHTML = newBarHtml;
7865
+ else if (newBarHtml) renderSessions();
8040
7866
  }
8041
7867
 
8042
7868
  function showCustomTagInput(sessId, event) {
@@ -8049,7 +7875,7 @@ function showCustomTagInput(sessId, event) {
8049
7875
  iw.className = 'ctag-input-wrap';
8050
7876
  iw.onclick = e => e.stopPropagation();
8051
7877
  iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
8052
- <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>`;
7878
+ <button class="ctag-ok" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
8053
7879
  wrap.appendChild(iw);
8054
7880
  const inp = iw.querySelector('input');
8055
7881
  inp.focus();
@@ -8061,11 +7887,11 @@ function showCustomTagInput(sessId, event) {
8061
7887
 
8062
7888
  function renderCustomTagsInline(sessId, tags) {
8063
7889
  const tagHtml = tags.map(t =>
8064
- `<span class="sr-ctag">${esc(t)}<span class="ctag-del" data-sess="${esc(sessId)}" data-tag="${esc(t)}" onclick="removeCustomTag(this.dataset.sess,this.dataset.tag,event)" title="Remove tag">✕</span></span>`
7890
+ `<span class="sr-ctag">${esc(t)}<span class="ctag-del" onclick="removeCustomTag('${esc(sessId)}','${esc(t)}',event)" title="Remove tag">✕</span></span>`
8065
7891
  ).join('');
8066
7892
  return `<div class="sr-custom-tags" data-sess="${esc(sessId)}" onclick="event.stopPropagation()">
8067
7893
  ${tagHtml}
8068
- <button class="ctag-add-btn" data-sess="${esc(sessId)}" onclick="showCustomTagInput(this.dataset.sess,event)" title="Add tag">+ tag</button>
7894
+ <button class="ctag-add-btn" onclick="showCustomTagInput('${esc(sessId)}',event)" title="Add tag">+ tag</button>
8069
7895
  </div>`;
8070
7896
  }
8071
7897
 
@@ -8080,14 +7906,14 @@ async function toggleErrDrawer(sessId, event) {
8080
7906
  try {
8081
7907
  const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
8082
7908
  if (!data.errors?.length) {
8083
- 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>`;
7909
+ drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
8084
7910
  return;
8085
7911
  }
8086
7912
  const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
8087
- 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>
7913
+ 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>
8088
7914
  <div class="err-drawer-body">${items}</div>`;
8089
7915
  } catch (err) {
8090
- 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>`;
7916
+ 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>`;
8091
7917
  }
8092
7918
  }
8093
7919
 
@@ -8121,7 +7947,7 @@ function buildCostHistogram() {
8121
7947
  const counts = new Array(BUCKETS).fill(0);
8122
7948
  for (const c of costs) { const i = Math.min(BUCKETS - 1, Math.floor((c - minC) / bucketSize)); counts[i]++; }
8123
7949
  const maxCount = Math.max(1, ...counts);
8124
- const fmt = v => v < 0.01 ? '$' + v.toFixed(4) : v < 1 ? '$' + v.toFixed(2) : '$' + v.toFixed(1);
7950
+ const fmt = v => v < 0.01 ? v.toFixed(4) : v < 1 ? '$' + v.toFixed(2) : '$' + v.toFixed(1);
8125
7951
  const bars = counts.map((n, i) => {
8126
7952
  const h = Math.max(2, Math.round((n / maxCount) * 46));
8127
7953
  const lo = minC + i * bucketSize; const hi = lo + bucketSize;
@@ -8187,25 +8013,26 @@ function esc(s) {
8187
8013
 
8188
8014
  function relTime(ts) {
8189
8015
  if (!ts) return '';
8190
- const abs = typeof ts === 'number' ? ts : (Number(ts) || new Date(ts).getTime() || 0);
8191
- const diff = Date.now() - abs;
8016
+ const diff = Date.now() - (typeof ts === 'number' ? ts : Number(ts) || new Date(ts).getTime() || 0);
8192
8017
  const s = Math.floor(diff / 1000);
8193
8018
  if (s < 5) return 'now';
8194
- if (s < 60) return s + 's ago';
8019
+ if (s < 60) return s + 's';
8195
8020
  const m = Math.floor(s / 60);
8196
- if (m < 60) return m + 'm ago';
8021
+ if (m < 60) return m + 'm';
8197
8022
  const h = Math.floor(m / 60);
8198
- if (h < 24) return h + 'h ago';
8199
- const d = Math.floor(h / 24);
8200
- if (d < 7) return d + 'd ago';
8201
- const dt = new Date(abs);
8202
- const mon = dt.toLocaleString('default', { month: 'short' });
8203
- const sameYear = dt.getFullYear() === new Date().getFullYear();
8204
- return sameYear ? mon + ' ' + dt.getDate() : mon + ' ' + dt.getDate() + ', ' + dt.getFullYear();
8023
+ if (h < 24) return h + 'h';
8024
+ return Math.floor(h / 24) + 'd';
8025
+ }
8026
+
8027
+ function shortPath(p) {
8028
+ if (!p) return '';
8029
+ const parts = p.replace(/\\/g, '/').split('/').filter(Boolean);
8030
+ if (parts.length <= 2) return p;
8031
+ return '…/' + parts.slice(-2).join('/');
8205
8032
  }
8206
8033
 
8207
8034
  function fmtDur(ms) {
8208
- if (!ms || ms < 1000) return '<1s';
8035
+ if (ms > 0 && ms < 1000) return '<1s';
8209
8036
  const s = Math.floor(ms / 1000);
8210
8037
  if (s < 60) return s + 's';
8211
8038
  const m = Math.floor(s / 60);
@@ -8402,7 +8229,7 @@ function renderMgOverview() {
8402
8229
  topEl.innerHTML = prSorted.map(n => `
8403
8230
  <div class="mg-bar-row">
8404
8231
  <div class="mg-bar-lbl" title="${esc(n.label)}">${esc(mgShortLabel(n.label))}</div>
8405
- <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>
8232
+ <div class="mg-bar-track"><div class="mg-bar-fill" style="width:${maxPR ? Math.round((n.score/maxPR)*100) : 0}%"></div></div>
8406
8233
  <div class="mg-bar-val">${n.score.toExponential(1)}</div>
8407
8234
  </div>`).join('');
8408
8235
  }
@@ -8427,7 +8254,7 @@ function renderMgOverview() {
8427
8254
  typeEl.innerHTML = typeEntries.map(([t, c]) => `
8428
8255
  <div class="mg-bar-row">
8429
8256
  <div class="mg-bar-lbl">${esc(t)}</div>
8430
- <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>
8257
+ <div class="mg-bar-track"><div class="mg-bar-fill mg" style="width:${Math.round((c/maxTC)*100)}%"></div></div>
8431
8258
  <div class="mg-bar-val">${c}</div>
8432
8259
  </div>`).join('');
8433
8260
  }
@@ -8479,11 +8306,9 @@ async function mgRebuild() {
8479
8306
  try {
8480
8307
  const res = await fetch('/api/monograph-build?dir=' + enc(DIR), { method: 'POST' });
8481
8308
  if (!res.ok) throw new Error('HTTP ' + res.status);
8482
- showToast('Rebuilding', 'Knowledge graph rebuild started', 'info');
8483
8309
  _mgLoaded = false;
8484
8310
  await loadMonograph();
8485
- showToast('Done', 'Knowledge graph rebuilt', 'ok');
8486
- } catch (e) { showToast('Error', e.message, 'err'); }
8311
+ } catch (_) {}
8487
8312
  btn.disabled = false; btn.textContent = 'REBUILD';
8488
8313
  }
8489
8314
 
@@ -8610,16 +8435,14 @@ function mgRunClient(id) {
8610
8435
  } else if (id === 'pagerank') {
8611
8436
  // Proper power-iteration PageRank (d=0.85, 50 iterations)
8612
8437
  const prMap = mgComputePageRank(g);
8613
- const allRanked = nodes.map(n => {
8438
+ const ranked = nodes.map(n => {
8614
8439
  const k = n.id || n.name || n.label || '';
8615
8440
  return { label: n.label || n.name || k, score: prMap[k] || 0 };
8616
- }).sort((a, b) => b.score - a.score);
8617
- const ranked = allRanked.slice(0, 20);
8441
+ }).sort((a, b) => b.score - a.score).slice(0, 20);
8618
8442
  const maxScore = ranked.length ? ranked[0].score : 1;
8619
8443
  html = `<table class="mg-table"><thead><tr><th>#</th><th>Node</th><th>PageRank</th><th style="width:180px">Weight</th></tr></thead><tbody>` +
8620
8444
  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('') +
8621
- `</tbody></table>` +
8622
- (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>` : '');
8445
+ `</tbody></table>`;
8623
8446
 
8624
8447
  } else if (id === 'deadcode') {
8625
8448
  const isolated = nodes.filter(n => (degMap[n.id || n.name || n.label || ''] || 0) === 0);
@@ -8651,8 +8474,7 @@ function mgRunClient(id) {
8651
8474
  <div class="mg-kv-card"><div class="mkv-k">Components</div><div class="mkv-v">${comps.length}</div></div>
8652
8475
  <div class="mg-kv-card"><div class="mkv-k">Largest</div><div class="mkv-v">${comps[0] ? comps[0].length : 0}</div></div>
8653
8476
  </div>` +
8654
- 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('') +
8655
- (comps.length > 5 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Showing 5 of ${comps.length} components</div>` : '');
8477
+ 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('');
8656
8478
 
8657
8479
  } else if (id === 'topo') {
8658
8480
  const inDeg = {}; const adj = {};
@@ -8729,16 +8551,14 @@ function mgRunClient(id) {
8729
8551
 
8730
8552
  } else if (id === 'betweenness') {
8731
8553
  const bc = mgApproxBetweenness(g);
8732
- const bcAll = Object.entries(bc).sort((a,b) => b[1]-a[1]);
8733
- const bcRanked = bcAll.slice(0,20);
8554
+ const bcRanked = Object.entries(bc).sort((a,b) => b[1]-a[1]).slice(0,20);
8734
8555
  const maxBC = bcRanked.length ? bcRanked[0][1] : 1;
8735
8556
  if (!bcRanked.length || maxBC === 0) { html = '<div class="loading-txt">Not enough graph data for betweenness</div>'; }
8736
8557
  else {
8737
8558
  html = `<div style="font-size:11px;color:var(--text-xs);margin-bottom:8px">Approximate betweenness centrality (sample-based BFS). High score = architectural bridge.</div>
8738
8559
  <table class="mg-table"><thead><tr><th>#</th><th>Node</th><th>Score</th><th style="width:160px">Weight</th></tr></thead><tbody>` +
8739
8560
  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('') +
8740
- `</tbody></table>` +
8741
- (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>` : '');
8561
+ `</tbody></table>`;
8742
8562
  }
8743
8563
 
8744
8564
  } else if (id === 'jaccard') {
@@ -8861,7 +8681,7 @@ function mgRenderAnalysisResult(toolId, text) {
8861
8681
  return `<div class="mgr-ranked-row">
8862
8682
  <span class="mgr-ranked-num">${r.rank}</span>
8863
8683
  <span class="mgr-ranked-name" title="${h(r.name)}">${h(r.name)}</span>
8864
- <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>
8684
+ <div class="mgr-ranked-bar-wrap"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
8865
8685
  <span class="mgr-ranked-val">${h(String(r.val))}</span>
8866
8686
  </div>`;
8867
8687
  }).join('')}</div>`;
@@ -8991,7 +8811,7 @@ function mgRenderAnalysisResult(toolId, text) {
8991
8811
  return `<div class="mgr-ranked-row">
8992
8812
  <span class="mgr-ranked-num">${r.rank}</span>
8993
8813
  <span class="mgr-ranked-name" title="${h(r.path)}">${h(name)}</span>
8994
- <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>
8814
+ <div class="mgr-ranked-bar-wrap"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
8995
8815
  <span class="mgr-ranked-val">${r.lines.toLocaleString()}</span>
8996
8816
  </div>`;
8997
8817
  }).join('')}</div>`;
@@ -9039,13 +8859,6 @@ async function mgRunServer(id) {
9039
8859
  }
9040
8860
 
9041
8861
  // ── QUERY TAB ──────────────────────────────────────────────
9042
- // Click any query result box to copy its text content
9043
- document.addEventListener('click', e => {
9044
- const el = e.target.closest('.mg-query-result');
9045
- if (!el || !el.textContent.trim()) return;
9046
- navigator.clipboard.writeText(el.textContent).then(() => showToast('Copied', 'Query result copied', 'ok')).catch(() => {});
9047
- });
9048
-
9049
8862
  function mgQuerySearch(q) {
9050
8863
  const res = document.getElementById('mg-q-search-result');
9051
8864
  if (!q.trim()) { res.style.display = 'none'; return; }
@@ -9240,7 +9053,7 @@ function mgRenderExport() {
9240
9053
  <div class="mg-export-card">
9241
9054
  <div class="mec-name">${esc(x.name)}</div>
9242
9055
  <div class="mec-desc">${esc(x.desc)}</div>
9243
- <button class="btn" title="Export graph as ${esc(x.name)}" onclick="mgExport('${esc(x.id)}')" style="margin-top:auto">EXPORT</button>
9056
+ <button class="btn" onclick="mgExport('${esc(x.id)}')" style="margin-top:auto">EXPORT</button>
9244
9057
  </div>`).join('');
9245
9058
  }
9246
9059
 
@@ -9504,8 +9317,8 @@ function mgRenderWiki() {
9504
9317
  // type pills
9505
9318
  const types = [...new Set(nodes.map(n => n.type || n.kind || 'unknown'))].sort();
9506
9319
  const pillsEl = document.getElementById('mg-wiki-pills');
9507
- pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" title="Show all node types" onclick="mgWikiFilterType('all')">All</span>` +
9508
- 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('');
9320
+ pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" onclick="mgWikiFilterType('all')">All</span>` +
9321
+ types.map(t => `<span class="mg-pill" data-type="${esc(t)}" onclick="mgWikiFilterType('${esc(t)}')">${esc(t)}</span>`).join('');
9509
9322
 
9510
9323
  mgRenderWikiList('');
9511
9324
  }
@@ -9576,11 +9389,9 @@ function mgWikiSearchDebounced(q) {
9576
9389
  apiFetch('/api/monograph-wiki-search?q=' + enc(q) + '&dir=' + enc(DIR))
9577
9390
  .then(d => {
9578
9391
  if (!d || !d.nodes || !d.nodes.length) return;
9579
- const serverNodes = d.nodes.slice(0, 50);
9580
- el._wikiData = serverNodes;
9581
- el.innerHTML = serverNodes.map((n, idx) => {
9392
+ el.innerHTML = d.nodes.slice(0, 50).map(n => {
9582
9393
  const lbl = n.label || n.name || '';
9583
- 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>`;
9394
+ 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>`;
9584
9395
  }).join('');
9585
9396
  }).catch(() => {});
9586
9397
  }
@@ -9605,9 +9416,9 @@ function mgWikiShowDetail(idx) {
9605
9416
  </div>
9606
9417
  ${contentPreview ? `<div class="mdp-content-preview">${esc(contentPreview)}</div>` : ''}
9607
9418
  <div class="mdp-actions">
9608
- ${contentPreview ? `<button class="btn" id="mg-copy-btn" title="Copy file content to clipboard" data-copy-idx="${idx}">COPY CONTENT</button>` : ''}
9609
- <button class="btn" title="Ask the AI to explain this node" data-explain-idx="${idx}">EXPLAIN</button>
9610
- <button class="btn" title="Find nodes related to this one in the graph" data-related-idx="${idx}">FIND RELATED</button>
9419
+ ${contentPreview ? `<button class="btn" id="mg-copy-btn" data-copy-idx="${idx}">COPY CONTENT</button>` : ''}
9420
+ <button class="btn" data-explain-idx="${idx}">EXPLAIN</button>
9421
+ <button class="btn" data-related-idx="${idx}">FIND RELATED</button>
9611
9422
  </div>
9612
9423
  <div class="mdp-explain-result" id="mg-explain-result" style="display:none"></div>
9613
9424
  <div id="mg-related-result" style="display:none;margin-top:10px"></div>`;
@@ -9642,26 +9453,6 @@ async function mgWikiExplain(nodeId, btn) {
9642
9453
  btn.disabled = false; btn.textContent = 'EXPLAIN';
9643
9454
  }
9644
9455
 
9645
- function mgWikiJumpToNode(nodeId) {
9646
- if (!_mgGraph) return;
9647
- const el = document.getElementById('mg-wiki-list');
9648
- const data = el._wikiData || (_mgGraph.nodes || []);
9649
- const idx = data.findIndex(n => (n.id || n.name || n.label || '') === nodeId);
9650
- if (idx >= 0) {
9651
- mgWikiShowDetail(idx);
9652
- document.getElementById('mg-wiki-detail')?.scrollIntoView({ behavior:'smooth', block:'nearest' });
9653
- } else {
9654
- // node not in current filtered list — search for it and jump
9655
- const searchEl = document.getElementById('mg-wiki-search');
9656
- if (searchEl) { searchEl.value = nodeId; mgRenderWikiList(nodeId); }
9657
- setTimeout(() => {
9658
- const newData = document.getElementById('mg-wiki-list')?._wikiData || [];
9659
- const newIdx = newData.findIndex(n => (n.id || n.name || n.label || '') === nodeId);
9660
- if (newIdx >= 0) mgWikiShowDetail(newIdx);
9661
- }, 100);
9662
- }
9663
- }
9664
-
9665
9456
  function mgWikiFindRelated(nodeId) {
9666
9457
  const el = document.getElementById('mg-related-result');
9667
9458
  if (!el || !_mgGraph) return;
@@ -9677,12 +9468,11 @@ function mgWikiFindRelated(nodeId) {
9677
9468
  hop1.forEach(v => { (adj[v] ? [...adj[v]] : []).forEach(w => { if (w !== nodeId && !hop1.includes(w)) hop2.add(w); }); });
9678
9469
  const related = [...hop1, ...hop2].slice(0, 20);
9679
9470
  if (!related.length) { el.innerHTML = '<div class="loading-txt">No related nodes found</div>'; el.style.display = 'block'; return; }
9680
- el.innerHTML = `<div style="font-size:11px;color:var(--text-xs);margin-bottom:6px">Related nodes (2-hop neighborhood) — click to open</div>` +
9471
+ el.innerHTML = `<div style="font-size:11px;color:var(--text-xs);margin-bottom:6px">Related nodes (2-hop neighborhood)</div>` +
9681
9472
  `<ul class="mg-node-list">` + related.map(k => {
9682
9473
  const n = (_mgGraph.nodes || []).find(x => (x.id || x.name || x.label || '') === k);
9683
9474
  const lbl = n ? (n.label || n.name || k) : k;
9684
- const typeIcon = n ? mgNodeIcon(n.type || n.kind) : '◈';
9685
- return `<li style="cursor:pointer;display:flex;align-items:center;gap:6px;padding:2px 0" onclick="mgWikiJumpToNode(${JSON.stringify(k).replace(/"/g, '&quot;')})" title="Open ${esc(lbl)}"><span style="opacity:0.6">${typeIcon}</span>${esc(lbl)}</li>`;
9475
+ return `<li>${esc(lbl)}</li>`;
9686
9476
  }).join('') + `</ul>`;
9687
9477
  el.style.display = 'block';
9688
9478
  }
@@ -9698,19 +9488,18 @@ async function mgRebuildDocs() {
9698
9488
  btn.disabled = true; btn.textContent = 'BUILDING…';
9699
9489
  try {
9700
9490
  await fetch('/api/monograph-build-docs?dir=' + enc(DIR), { method:'POST' });
9701
- showToast('Building', 'Documentation build started…', 'info');
9702
9491
  // poll until done (max 60 attempts = ~2 minutes)
9703
9492
  let polls = 0;
9704
9493
  const poll = async () => {
9705
- if (++polls > 60) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; showToast('Timeout', 'Doc build is taking longer than expected', 'warn'); return; }
9494
+ if (++polls > 60) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; return; }
9706
9495
  try {
9707
9496
  const d = await apiFetch('/api/monograph-build-docs-status?dir=' + enc(DIR));
9708
- if (d && d.done) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; mgWikiRefresh(); showToast('Done', 'Documentation built', 'ok'); return; }
9497
+ if (d && d.done) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; mgWikiRefresh(); return; }
9709
9498
  } catch (_) {}
9710
9499
  setTimeout(poll, 2000);
9711
9500
  };
9712
9501
  poll();
9713
- } catch (e) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; showToast('Error', e.message, 'err'); }
9502
+ } catch (e) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; }
9714
9503
  }
9715
9504
 
9716
9505
  // ── memory CRUD ────────────────────────────────────────────
@@ -9748,41 +9537,6 @@ async function loadMemoriesTab() {
9748
9537
  }
9749
9538
  }
9750
9539
 
9751
- function filterMemList(q) {
9752
- const lq = (q || '').toLowerCase();
9753
- let totalVisible = 0;
9754
- document.querySelectorAll('#mem-list-pane .mem-item').forEach(el => {
9755
- const text = (el.textContent || '').toLowerCase();
9756
- const show = !lq || text.includes(lq);
9757
- el.style.display = show ? '' : 'none';
9758
- if (show) totalVisible++;
9759
- });
9760
- document.querySelectorAll('#mem-list-pane .mem-type-hdr').forEach(hdr => {
9761
- let next = hdr.nextElementSibling;
9762
- let anyVisible = false;
9763
- while (next && !next.classList.contains('mem-type-hdr')) {
9764
- if (next.style.display !== 'none') anyVisible = true;
9765
- next = next.nextElementSibling;
9766
- }
9767
- hdr.style.display = anyVisible ? '' : 'none';
9768
- });
9769
- const list = document.getElementById('mem-list-pane');
9770
- const noRes = document.getElementById('mem-filter-noresult');
9771
- if (lq && totalVisible === 0 && _memFiles.length > 0) {
9772
- if (!noRes) {
9773
- const el = document.createElement('div');
9774
- el.id = 'mem-filter-noresult';
9775
- el.style.cssText = 'padding:12px 14px;color:var(--text-lo);font-size:12px';
9776
- el.textContent = 'No memories match "' + q.slice(0, 30) + '"';
9777
- list.appendChild(el);
9778
- } else {
9779
- noRes.textContent = 'No memories match "' + q.slice(0, 30) + '"';
9780
- }
9781
- } else if (noRes) {
9782
- noRes.remove();
9783
- }
9784
- }
9785
-
9786
9540
  function _renderMemList() {
9787
9541
  const list = document.getElementById('mem-list-pane');
9788
9542
  if (!list) return;
@@ -9798,8 +9552,7 @@ function _renderMemList() {
9798
9552
  const col = _MEM_COLORS[type] || _MEM_COLOR_FALLBACK;
9799
9553
  const fname = f.filename || f.name || '?';
9800
9554
  const active = _selMemFilename === fname ? ' active' : '';
9801
- const memTitle = [f.description, f.name || fname].filter(Boolean).join(' ');
9802
- return '<div class="mem-item' + active + '" data-filename="' + esc(fname) + '" onclick="selectMem(this.dataset.filename)" title="' + esc(memTitle) + '">' +
9555
+ return '<div class="mem-item' + active + '" data-filename="' + esc(fname) + '" onclick="selectMem(this.dataset.filename)">' +
9803
9556
  '<span class="mem-type-dot" style="background:' + esc(col) + ';flex-shrink:0"></span>' +
9804
9557
  '<span class="mem-item-name">' + esc(f.name || fname.replace('.md', '')) + '</span>' +
9805
9558
  '</div>';
@@ -9818,17 +9571,17 @@ function selectMem(filename) {
9818
9571
  const rawBody = f.body || f.content || '';
9819
9572
  const bodyHtml = rawBody
9820
9573
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
9821
- .replace(/\*\*(.+?)\*\*/g, (_, g) => '<strong>' + g + '</strong>')
9822
- .replace(/^#{1,3} (.+)/gm, (_, g) => '<div style="font-weight:600;color:var(--text-hi);margin:6px 0 2px">' + g + '</div>')
9823
- .replace(/^- (.+)/gm, (_, g) => '<div style="padding-left:12px">• ' + g + '</div>');
9574
+ .replace(/\*\*(.+?)\*\*/g, (_, g) => '<strong>' + esc(g) + '</strong>')
9575
+ .replace(/^#{1,3} (.+)/gm, (_, g) => '<div style="font-weight:600;color:var(--text-hi);margin:6px 0 2px">' + esc(g) + '</div>')
9576
+ .replace(/^- (.+)/gm, (_, g) => '<div style="padding-left:12px">• ' + esc(g) + '</div>');
9824
9577
  const srcBadge = f.source === 'backend'
9825
9578
  ? '<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>'
9826
9579
  : '';
9827
9580
  const actions = (f.readonly || f.source === 'backend')
9828
9581
  ? '<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>'
9829
9582
  : '<div class="mem-actions">' +
9830
- '<button class="btn" title="Edit this memory entry" onclick="openEditMemModal(' + JSON.stringify(filename).replace(/"/g, '&quot;') + ')">&#x270E; Edit</button>' +
9831
- '<button class="btn" title="Delete this memory entry" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename).replace(/"/g, '&quot;') + ')">&#x2715; Delete</button>' +
9583
+ '<button class="btn" onclick="openEditMemModal(' + JSON.stringify(filename) + ')">&#x270E; Edit</button>' +
9584
+ '<button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">&#x2715; Delete</button>' +
9832
9585
  '</div>';
9833
9586
  detail.innerHTML =
9834
9587
  '<span class="mem-badge" style="background:' + col + '22;color:' + col + '">' + esc(f.type || '?') + '</span>' + srcBadge +
@@ -9938,7 +9691,7 @@ function _renderSwarmRunList() {
9938
9691
  '<span class="swarm-topo-pill">' + esc(topo) + '</span>' +
9939
9692
  (live ? '<span class="swarm-live">⬤ LIVE</span>' : '') +
9940
9693
  '</div>' +
9941
- '<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>' +
9694
+ '<div style="font-size:11px;color:var(--text-lo);margin-top:2px">' + (r.agentCount || 0) + ' agents · ' + relTime(r.startedAt || r.created_at) + '</div>' +
9942
9695
  '</div>';
9943
9696
  }).join('');
9944
9697
  }
@@ -9951,7 +9704,7 @@ async function selectSwarmRun(idx) {
9951
9704
  if (!detail) return;
9952
9705
  detail.innerHTML =
9953
9706
  '<div style="margin-bottom:10px">' +
9954
- '<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>' +
9707
+ '<div style="font-size:13px;font-weight:600;color:var(--text-hi)">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
9955
9708
  '<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + esc(run.consensus || '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
9956
9709
  '</div>' +
9957
9710
  '<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>' +
@@ -10028,16 +9781,14 @@ function _renderSwarmAgents(run) {
10028
9781
  if (!el) return;
10029
9782
  const agents = run.agents || [];
10030
9783
  if (!agents.length) { el.innerHTML = ''; return; }
10031
- const shownA = agents.slice(0, 15);
10032
9784
  el.innerHTML = '<div class="m-group-title" style="margin-bottom:4px">Agents</div>' +
10033
- shownA.map(a =>
9785
+ agents.slice(0, 15).map(a =>
10034
9786
  '<div style="display:flex;gap:10px;padding:3px 0;font-size:11px;border-bottom:1px solid var(--border)">' +
10035
- '<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>' +
9787
+ '<span style="color:var(--text-lo);font-family:var(--mono);min-width:70px">' + esc((a.id || '').toString().slice(0, 10)) + '</span>' +
10036
9788
  '<span style="color:var(--text-hi)">' + esc(a.type || a.role || 'worker') + '</span>' +
10037
9789
  '<span style="margin-left:auto;color:var(--text-xs)">' + esc(a.status || '—') + '</span>' +
10038
9790
  '</div>'
10039
- ).join('') +
10040
- (agents.length > 15 ? '<div style="font-size:11px;color:var(--text-xs);padding:3px 0">+' + (agents.length - 15) + ' more agents</div>' : '');
9791
+ ).join('');
10041
9792
  }
10042
9793
 
10043
9794
  async function _loadSwarmEvents(swarmId) {
@@ -10048,13 +9799,11 @@ async function _loadSwarmEvents(swarmId) {
10048
9799
  const data = await apiFetch('/api/swarm-events?agentId=' + enc(swarmId) + '&dir=' + enc(DIR));
10049
9800
  const events = Array.isArray(data) ? data : (data.events || []);
10050
9801
  if (!events.length) { el.innerHTML = ''; return; }
10051
- const shownEv = events.slice(-40);
10052
- 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>' +
10053
- shownEv.map(e =>
9802
+ el.innerHTML = '<div class="m-group-title" style="margin-bottom:3px">Events</div>' +
9803
+ events.slice(-40).map(e =>
10054
9804
  '<div style="color:var(--text-lo);padding:2px 0;border-bottom:1px solid var(--border)">' +
10055
- '<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>' +
10056
- ' <span style="color:var(--text-mid)">' + esc(e.type || e.kind || '?') + '</span> ' +
10057
- '<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>' +
9805
+ esc(relTime(e.ts || e.timestamp)) + ' <span style="color:var(--text-mid)">' + esc(e.type || e.kind || '?') + '</span> ' +
9806
+ esc((e.message || e.data || '').toString().slice(0, 70)) +
10058
9807
  '</div>'
10059
9808
  ).join('');
10060
9809
  el.scrollTop = el.scrollHeight;
@@ -10118,25 +9867,23 @@ function _renderChunks(list) {
10118
9867
  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>';
10119
9868
  return;
10120
9869
  }
10121
- const shown = list.slice(0, 200);
10122
- grid.innerHTML = shown.map(c => {
9870
+ grid.innerHTML = list.slice(0, 200).map(c => {
10123
9871
  const src = (c.source || c.file || '').split('/').slice(-2).join('/');
10124
9872
  const excerpt = (c.content || c.text || c.body || '').slice(0, 220);
10125
9873
  const ns = c.namespace || c.type || '';
10126
- const chunkId = JSON.stringify(c.id || c.path || c.source || '').replace(/"/g, '&quot;');
10127
- const chunkContent = JSON.stringify(c.content || c.text || c.body || '').replace(/"/g, '&quot;');
10128
- const chunkSrc = JSON.stringify(src).replace(/"/g, '&quot;');
9874
+ const chunkId = JSON.stringify(c.id || c.path || c.source || '');
9875
+ const chunkContent = JSON.stringify(c.content || c.text || c.body || '');
9876
+ const chunkSrc = JSON.stringify(src);
10129
9877
  return '<div class="chunk-card" data-search="' + esc((src + ' ' + excerpt + ' ' + ns).toLowerCase()) + '">' +
10130
9878
  '<div class="chunk-src">' + esc(src || '—') + '</div>' +
10131
- '<div class="chunk-excerpt" title="' + esc(c.content || c.text || c.body || '') + '">' + esc(excerpt) + ((c.content || c.text || c.body || '').length > 220 ? '…' : '') + '</div>' +
9879
+ '<div class="chunk-excerpt">' + esc(excerpt) + '</div>' +
10132
9880
  '<div class="chunk-footer">' +
10133
9881
  (ns ? '<span class="chunk-ns">' + esc(ns) + '</span>' : '') +
10134
- '<button class="btn" title="Edit this chunk" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
10135
- '<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>' +
9882
+ '<button class="btn" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
9883
+ '<button class="btn" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunk(' + chunkId + ')">✕</button>' +
10136
9884
  '</div>' +
10137
9885
  '</div>';
10138
- }).join('') +
10139
- (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>' : '');
9886
+ }).join('');
10140
9887
  }
10141
9888
 
10142
9889
  function openChunkEdit(id, content, srcLabel) {
@@ -10210,33 +9957,9 @@ async function buildKnowledgeDocs() {
10210
9957
 
10211
9958
  function filterChunks(q) {
10212
9959
  const lq = q.toLowerCase();
10213
- const cards = document.querySelectorAll('#chunks-grid .chunk-card');
10214
- let visible = 0;
10215
- cards.forEach(el => {
10216
- const show = !lq || (el.dataset.search || '').includes(lq);
10217
- el.style.display = show ? '' : 'none';
10218
- if (show) visible++;
9960
+ document.querySelectorAll('#chunks-grid .chunk-card').forEach(el => {
9961
+ el.style.display = (!lq || (el.dataset.search || '').includes(lq)) ? '' : 'none';
10219
9962
  });
10220
- const countEl = document.getElementById('chunk-count-val');
10221
- if (countEl && lq) countEl.textContent = visible + ' / ' + cards.length;
10222
- else if (countEl) countEl.textContent = cards.length.toLocaleString();
10223
- // zero-results empty state
10224
- const grid = document.getElementById('chunks-grid');
10225
- const noRes = document.getElementById('chunks-filter-noresult');
10226
- if (lq && visible === 0 && cards.length > 0) {
10227
- if (!noRes) {
10228
- const el = document.createElement('div');
10229
- el.id = 'chunks-filter-noresult';
10230
- el.className = 'empty';
10231
- el.style.cssText = 'padding:20px 0;font-size:13px';
10232
- el.textContent = 'No chunks match "' + q.slice(0, 40) + '"';
10233
- grid.appendChild(el);
10234
- } else {
10235
- noRes.textContent = 'No chunks match "' + q.slice(0, 40) + '"';
10236
- }
10237
- } else if (noRes) {
10238
- noRes.remove();
10239
- }
10240
9963
  }
10241
9964
 
10242
9965
  async function deleteChunk(id) {
@@ -10263,28 +9986,8 @@ async function loadAgentGraphTab() {
10263
9986
  const bar = document.getElementById('ag-summary-bar');
10264
9987
  if (bar) bar.innerHTML = '<div class="loading-txt">Loading…</div>';
10265
9988
  try {
10266
- const raw = await apiFetch('/api/graph?dir=' + enc(DIR));
10267
- // API returns { nodes, edges } — normalize into the shape renderers expect
10268
- const sesNodes = (raw.nodes || []).filter(n => n.type === 'session');
10269
- const agNodes = (raw.nodes || []).filter(n => n.type === 'agenttype');
10270
- const sessions = sesNodes.map(n => ({
10271
- id: n.id,
10272
- file: n.id,
10273
- turns: n.turns || 0,
10274
- toolCount: n.totalTools || 0,
10275
- spawnCount: Object.values(n.agentSpawns || {}).reduce((a, b) => a + b, 0),
10276
- cost: n.cost || 0,
10277
- agentTypes: n.agentSpawns || {},
10278
- tools: n.toolCounts || {},
10279
- }));
10280
- _agData = {
10281
- sessions,
10282
- sessionCount: sessions.length,
10283
- agentTypes: agNodes.length,
10284
- totalSpawns: agNodes.reduce((a, n) => a + (n.totalSpawns || 0), 0),
10285
- totalToolCalls: sesNodes.reduce((a, n) => a + (n.totalTools || 0), 0),
10286
- totalCost: sesNodes.reduce((a, n) => a + (n.cost || 0), 0),
10287
- };
9989
+ const data = await apiFetch('/api/graph?dir=' + enc(DIR));
9990
+ _agData = data;
10288
9991
  _renderAgSummary();
10289
9992
  _renderAgSessList();
10290
9993
  } catch (e) {
@@ -10315,27 +10018,15 @@ function _renderAgSessList() {
10315
10018
  const el = document.getElementById('ag-sess-list');
10316
10019
  if (!_agData || !el) return;
10317
10020
  const sessions = _agData.sessions || [];
10318
- 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; }
10319
- el.innerHTML =
10320
- '<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">' +
10321
- '<div id="ag-sess-rows">' +
10322
- sessions.map((s, i) =>
10323
- '<div class="sess-row" style="margin-bottom:4px" data-sid="' + esc(s.id || s.file || '') + '" onclick="selectAgSession(' + i + ')">' +
10324
- '<div class="sr-top">' +
10325
- '<div class="sr-prompt" style="font-size:11px" title="' + esc(s.id || s.file || '') + '">' + esc((s.id || s.file || '').slice(-16)) + '</div>' +
10326
- '</div>' +
10327
- '<div style="font-size:10px;color:var(--text-lo);margin-top:1px">' + (s.spawnCount || 0) + ' spawns · ' + (s.toolCount || 0) + ' tools</div>' +
10328
- '</div>'
10329
- ).join('') +
10330
- '</div>';
10331
- }
10332
-
10333
- function filterAgSessions(q) {
10334
- const lq = (q || '').toLowerCase();
10335
- document.querySelectorAll('#ag-sess-rows .sess-row').forEach(el => {
10336
- const sid = (el.dataset.sid || '').toLowerCase();
10337
- el.style.display = (!lq || sid.includes(lq)) ? '' : 'none';
10338
- });
10021
+ if (!sessions.length) { el.innerHTML = '<div class="empty" style="font-size:12px">No sessions</div>'; return; }
10022
+ el.innerHTML = sessions.map((s, i) =>
10023
+ '<div class="sess-row" style="margin-bottom:4px" onclick="selectAgSession(' + i + ')">' +
10024
+ '<div class="sr-top">' +
10025
+ '<div class="sr-prompt" style="font-size:11px">' + esc((s.id || s.file || '').slice(-16)) + '</div>' +
10026
+ '</div>' +
10027
+ '<div style="font-size:10px;color:var(--text-lo);margin-top:1px">' + (s.spawnCount || 0) + ' spawns · ' + (s.toolCount || 0) + ' tools</div>' +
10028
+ '</div>'
10029
+ ).join('');
10339
10030
  }
10340
10031
 
10341
10032
  function selectAgSession(idx) {
@@ -10344,10 +10035,8 @@ function selectAgSession(idx) {
10344
10035
  document.querySelectorAll('#ag-sess-list .sess-row').forEach((el, i) => el.classList.toggle('active', i === idx));
10345
10036
  const detail = document.getElementById('ag-detail');
10346
10037
  if (!detail) return;
10347
- const agAllArr = Object.entries(s.agentTypes || {}).sort((a, b) => b[1] - a[1]);
10348
- const toolAllArr = Object.entries(s.tools || {}).sort((a, b) => b[1] - a[1]);
10349
- const agArr = agAllArr.slice(0, 12);
10350
- const toolArr = toolAllArr.slice(0, 15);
10038
+ const agArr = Object.entries(s.agentTypes || {}).sort((a, b) => b[1] - a[1]).slice(0, 12);
10039
+ const toolArr = Object.entries(s.tools || {}).sort((a, b) => b[1] - a[1]).slice(0, 15);
10351
10040
  const maxAg = agArr.length ? Math.max(...agArr.map(x => x[1])) : 1;
10352
10041
  const maxTool = toolArr.length ? Math.max(...toolArr.map(x => x[1])) : 1;
10353
10042
  detail.innerHTML =
@@ -10357,20 +10046,20 @@ function selectAgSession(idx) {
10357
10046
  '<span>Tools: <b>' + (s.toolCount || 0) + '</b></span>' +
10358
10047
  (s.cost != null ? '<span style="color:var(--accent)">$' + Number(s.cost).toFixed(4) + '</span>' : '') +
10359
10048
  '</div>' +
10360
- (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>' +
10049
+ (agArr.length ? '<div class="m-group-title" style="margin-bottom:5px">Agent Types</div>' +
10361
10050
  agArr.map(function(entry) {
10362
10051
  var type = entry[0], count = entry[1];
10363
10052
  return '<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">' +
10364
- '<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi)" title="' + esc(type) + '">' + esc(type) + '</div>' +
10053
+ '<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi)">' + esc(type) + '</div>' +
10365
10054
  '<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>' +
10366
10055
  '<div style="width:22px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">' + count + '</div>' +
10367
10056
  '</div>';
10368
10057
  }).join('') : '') +
10369
- (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>' +
10058
+ (toolArr.length ? '<div class="m-group-title" style="margin-bottom:5px;margin-top:14px">Top Tools</div>' +
10370
10059
  toolArr.map(function(entry) {
10371
10060
  var tool = entry[0], count = entry[1];
10372
10061
  return '<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">' +
10373
- '<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>' +
10062
+ '<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>' +
10374
10063
  '<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>' +
10375
10064
  '<div style="width:22px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">' + count + '</div>' +
10376
10065
  '</div>';
@@ -10434,10 +10123,10 @@ function mmSwitchTab(tab) {
10434
10123
  const body = document.getElementById('mm-body');
10435
10124
  if (!body) return;
10436
10125
  if (tab === 'orgs') mmRenderOrgs(body);
10437
- else if (tab === 'skills') { mmRenderSkills(body); setTimeout(() => document.getElementById('mm-skill-filter-input')?.focus(), 50); }
10438
- else if (tab === 'loops') mmRenderLoops(body);
10126
+ else if (tab === 'skills') mmRenderSkills(body);
10127
+ else if (tab === 'loops') { mmRenderLoops(body); body.insertAdjacentHTML('afterbegin', '<button class="btn" onclick="mmSwitchTab(\'loops\')" style="margin-bottom:10px;font-size:11px">↺ Refresh</button>'); }
10439
10128
  else if (tab === 'createorg') mmRenderCreateOrg(body);
10440
- else if (tab === 'metrics') mmRenderMetrics(body);
10129
+ else if (tab === 'metrics') { mmRenderMetrics(body); body.insertAdjacentHTML('afterbegin', '<button class="btn" onclick="mmSwitchTab(\'metrics\')" style="margin-bottom:10px;font-size:11px">↺ Refresh</button>'); }
10441
10130
  else if (tab === 'graph') mmRenderGraph(body);
10442
10131
  }
10443
10132
 
@@ -10446,9 +10135,9 @@ function mmRenderOrgs(body) {
10446
10135
  if (!orgs.length) { body.innerHTML = '<div class="empty">No orgs found. Run /mastermind:createorg to create one.</div>'; return; }
10447
10136
  body.innerHTML = orgs.map(o => {
10448
10137
  const running = o.running;
10449
- return `<div class="mm-skill-item" onclick="closeMastermind();v2SelectOrg(${JSON.stringify(o.name).replace(/"/g, '&quot;')});switchView('orgs')">
10138
+ return `<div class="mm-skill-item" onclick="closeMastermind();v2SelectOrg(${JSON.stringify(o.name)});switchView('orgs')">
10450
10139
  <span class="mm-skill-name">${esc(o.name)}</span>
10451
- <span class="mm-skill-desc" title="${esc(o.goal || '')}">${esc((o.goal || '').slice(0, 60))}${(o.goal || '').length > 60 ? '…' : ''} ${running ? '⬤ LIVE' : ''}</span>
10140
+ <span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${running ? '⬤ LIVE' : ''}</span>
10452
10141
  </div>`;
10453
10142
  }).join('');
10454
10143
  }
@@ -10457,8 +10146,8 @@ let _mmSkillFilter = '';
10457
10146
  function mmRenderSkills(body) {
10458
10147
  const q = _mmSkillFilter.toLowerCase();
10459
10148
  const filtered = q ? _MM_SKILLS_CATALOG.filter(s => s.name.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : _MM_SKILLS_CATALOG;
10460
- 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>` +
10461
- filtered.map(s => `<div class="mm-skill-item" title="Click to copy: /${esc(s.name)}" onclick="navigator.clipboard.writeText(${JSON.stringify(s.name).replace(/"/g, '&quot;')}).then(()=>showToast('Copied',${JSON.stringify(s.name).replace(/"/g, '&quot;')},'ok'))">
10149
+ 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>` +
10150
+ filtered.map(s => `<div class="mm-skill-item" onclick="navigator.clipboard.writeText(${JSON.stringify(s.name)}).then(()=>showToast('Copied',${JSON.stringify(s.name)},'ok'))">
10462
10151
  <span class="mm-skill-name">${esc(s.name)}</span>
10463
10152
  <span class="mm-skill-desc">${esc(s.desc)}</span>
10464
10153
  </div>`).join('');
@@ -10470,66 +10159,30 @@ async function mmRenderLoops(body) {
10470
10159
  const data = await apiFetch('/api/loops?dir=' + enc(DIR));
10471
10160
  const loops = Array.isArray(data) ? data : (data.loops || []);
10472
10161
  if (!loops.length) { body.innerHTML = '<div class="empty">No active loops. Use /mastermind:autodev --tillend to start one.</div>'; return; }
10473
- const mmHasRepeat = loops.some(l => l.source === '_repeat.md');
10474
- const mmRepeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
10475
- const mmDeduped = loops.filter(l => {
10476
- if (l.source === 'scheduled_tasks_lock' && mmHasRepeat) return false;
10477
- if (l.source !== 'schedule_wakeup_hook') return true;
10478
- const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
10479
- return !m || !mmRepeatPrompts.has(m[1].trim());
10480
- });
10481
- 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>';
10482
- body.innerHTML = _mmLoopsRefresh + mmDeduped.map(l => {
10483
- const isHilMm = l.status === 'hil:pending';
10484
- const isTillendMm = l.type === 'tillend';
10485
- const curRep = l.currentRep || 0;
10486
- const maxReps = l.maxReps || 0;
10487
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
10488
- const isExplicitlyActiveMm = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
10489
- const _STALE = 2 * 60 * 60 * 1000;
10490
- const isOverdueMm = !l.status?.startsWith('hil') && !isExplicitlyActiveMm &&
10491
- nextAt > 0 && nextAt <= Date.now();
10492
- const isStaledActiveMm = isExplicitlyActiveMm && nextAt > 0 && (Date.now() - nextAt) > _STALE;
10493
- const isFinishedMm = isOverdueMm || isStaledActiveMm ||
10494
- (!isExplicitlyActiveMm && maxReps > 0 && curRep >= maxReps) ||
10495
- l.status === 'finished' || l.status === 'done' ||
10496
- l.status === 'complete' || l.status === 'completed' || l.status === 'expired';
10497
- const running = !isFinishedMm && l.status !== 'stopped' && l.status !== 'paused';
10498
- const _mmLp = (function(_l) {
10499
- if (_l.command) return { userPrompt: _l.prompt || '', command: _l.command };
10500
- const full = _l.prompt || '';
10501
- const cmdM = full.match(/^(\/\S+)/);
10502
- if (!cmdM) return { userPrompt: full, command: '' };
10503
- const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
10504
- let ti = 0;
10505
- while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
10506
- ti++;
10507
- if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) ti++;
10508
- }
10509
- return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1] };
10510
- })(l);
10511
- const name = (l.name || _mmLp.userPrompt || _mmLp.command || 'loop').slice(0, 60);
10512
- const ms = nextAt ? nextAt - Date.now() : 0;
10513
- const cdown = ms > 0 ? (Math.floor(ms/60000) + 'm ' + Math.floor((ms%60000)/1000) + 's') : '';
10514
- const intervalMm = fmtInterval(l.interval || l.schedule);
10515
- const runCount = isTillendMm
10516
- ? `run ${curRep} / ∞${maxReps ? ' (cap: ' + maxReps + ')' : ''}`
10517
- : (maxReps > 0 ? `run ${curRep} / ${maxReps}` : (curRep ? `run ${curRep}` : ''));
10518
- const statusLabel = isHilMm ? '⚠ HIL' : (running ? 'active' : (isFinishedMm ? 'done' : 'stopped'));
10519
- const statusColor = isHilMm ? 'oklch(75% 0.16 60)' : '';
10520
- const typeIco = isTillendMm ? '∞ ' : '↺ ';
10521
- const _sMs = l.startedAt ? (typeof l.startedAt === 'number' ? l.startedAt : new Date(l.startedAt).getTime()) : 0;
10522
- const ageMs = _sMs > 0 && _sMs < Date.now() ? Date.now() - _sMs : 0;
10523
- const ageStr = ageMs > 0 ? fmtDur(ageMs) : '';
10524
- const metaParts = [intervalMm, ageStr ? 'running ' + ageStr : '', cdown ? 'next in ' + cdown : '', runCount].filter(Boolean).join(' · ');
10162
+ body.innerHTML = loops.map(l => {
10163
+ const maxReps = l.maxReps || 0;
10164
+ const curRep = l.currentRep || 0;
10165
+ const isTillend = !maxReps || l.loopType === 'tillend' || String(l.command || '').includes('--tillend');
10166
+ const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
10167
+ const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
10168
+ const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
10169
+ const isStaledActive = isExplicitlyActive && nextAt > 0 && (Date.now() - nextAt) > LOOP_STALE_MS;
10170
+ const isFinished = (maxReps > 0 && curRep >= maxReps) || ['finished','done','complete','completed','expired'].includes(l.status) || isOverdue || isStaledActive;
10171
+ const isHil = l.status === 'hil:pending';
10172
+ const running = !isFinished && !isHil && l.status !== 'stopped' && l.status !== 'paused';
10173
+ const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
10174
+ const ms = nextAt ? nextAt - Date.now() : 0;
10175
+ const cdown = ms > 0 ? (Math.floor(ms/60000) + 'm ' + Math.floor((ms%60000)/1000) + 's') : (running ? 'running' : isFinished ? 'done' : 'stopped');
10176
+ const typeBadge = isTillend ? '∞' : '↺';
10525
10177
  return `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
10526
10178
  <div style="display:flex;align-items:center;gap:8px">
10527
- <span style="font-size:13px;color:var(--text-hi);flex:1">${typeIco}${esc(name)}</span>
10528
- <span class="ss-pill ${running && !isHilMm ? 'on' : ''}" style="${statusColor ? 'color:'+statusColor+';background:oklch(65% 0.15 60 / 0.15);border:none' : ''}">${statusLabel}</span>
10529
- ${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||'').replace(/"/g, '&quot;')});mmSwitchTab('loops')">■ Stop</button>` : ''}
10179
+ <span style="font-size:13px;color:var(--accent)">${typeBadge}</span>
10180
+ <span style="font-size:13px;color:var(--text-hi);flex:1">${esc(name)}</span>
10181
+ ${isHil ? '<span style="color:oklch(78% 0.18 80);font-size:10px">⚠ HIL</span>' : ''}
10182
+ <span class="ss-pill ${running ? 'on' : ''}">${running ? 'active' : isFinished ? 'done' : 'stopped'}</span>
10183
+ ${(running || isHil) ? `<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>` : ''}
10530
10184
  </div>
10531
- <div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(metaParts)}</div>
10532
- ${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>` : ''}
10185
+ <div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(fmtInterval(l.interval||l.schedule||''))} ${cdown ? '· ' + cdown : ''} ${curRep != null ? '· run ' + curRep + (isTillend ? '/∞' : maxReps ? '/'+maxReps : '') : ''}</div>
10533
10186
  </div>`;
10534
10187
  }).join('');
10535
10188
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -10543,17 +10196,17 @@ function mmRenderCreateOrg(body) {
10543
10196
  <div><div class="le-lbl">Org Name</div><input id="mco-name" class="filter-input" placeholder="my-team"></div>
10544
10197
  <div><div class="le-lbl">Goal</div><input id="mco-goal" class="filter-input" placeholder="Build and ship features autonomously"></div>
10545
10198
  <div><div class="le-lbl">Topology</div>
10546
- <select id="mco-topo" class="filter-input" title="Agent coordination topology" style="cursor:pointer">
10199
+ <select id="mco-topo" class="filter-input" style="cursor:pointer">
10547
10200
  ${topos.map(t => `<option>${t}</option>`).join('')}
10548
10201
  </select>
10549
10202
  </div>
10550
10203
  <div><div class="le-lbl">Adapter</div>
10551
- <select id="mco-adapter" class="filter-input" title="Agent communication adapter" style="cursor:pointer">
10204
+ <select id="mco-adapter" class="filter-input" style="cursor:pointer">
10552
10205
  ${adapters.map(a => `<option>${a}</option>`).join('')}
10553
10206
  </select>
10554
10207
  </div>
10555
- <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>
10556
- <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>
10208
+ <div><div class="le-lbl">Max Agents</div><input id="mco-agents" class="filter-input" type="number" value="8" min="1" max="50"></div>
10209
+ <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>
10557
10210
  <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>
10558
10211
  </div>`;
10559
10212
  }
@@ -10597,7 +10250,6 @@ async function mmRenderMetrics(body) {
10597
10250
  </div>
10598
10251
  <div class="m-group-title" style="margin-bottom:8px">Swarm</div>
10599
10252
  <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>
10600
- <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>
10601
10253
  `;
10602
10254
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
10603
10255
  }
@@ -10623,13 +10275,13 @@ async function mmRenderGraph(body) {
10623
10275
  <div class="chunk-stat"><div class="chunk-stat-val">${Object.keys(nodeTypes).length}</div><div class="chunk-stat-lbl">Types</div></div>
10624
10276
  </div>
10625
10277
  ${topNodes.length ? `<div class="m-group-title" style="margin-bottom:6px">God Nodes</div>
10626
- ${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=''">
10278
+ ${topNodes.map(n => `<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)">
10627
10279
  <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>
10628
- <span style="color:var(--text-lo);font-family:var(--mono);font-size:11px">${n.degree ?? '—'} ↗</span>
10280
+ <span style="color:var(--text-lo);font-family:var(--mono);font-size:11px">${n.degree ?? '—'}</span>
10629
10281
  </div>`).join('')}` : ''}
10630
10282
  <div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
10631
- <button class="btn" title="Switch to the Monograph view" onclick="switchView('monograph');closeMastermind()">Open Monograph →</button>
10632
- <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>
10283
+ <button class="btn" onclick="switchView('monograph');closeMastermind()">Open Monograph →</button>
10284
+ <button class="btn" onclick="fetch('/api/monograph-build?dir='+enc(DIR),{method:'POST'}).then(()=>showToast('Building','Graph rebuild started','ok'))">↺ Rebuild Graph</button>
10633
10285
  </div>
10634
10286
  `;
10635
10287
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }