@monoes/monomindcli 1.11.13 → 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 +97 -20
  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 +926 -1268
  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>
@@ -2603,8 +2636,7 @@ function expandGroup(el) {
2603
2636
 
2604
2637
  function renderFeedEntry(ev) {
2605
2638
  const ts = ev.ts ? relTime(ev.ts) : '';
2606
- const tsTitle = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '';
2607
- let lbl = '', detail = '', lblTitle = '', id = ev.id || ev.uuid || '';
2639
+ let lbl = '', detail = '', id = ev.id || ev.uuid || '';
2608
2640
  let catCls, ico;
2609
2641
 
2610
2642
  if (ev.kind === 'tool') {
@@ -2614,21 +2646,20 @@ function renderFeedEntry(ev) {
2614
2646
  } else {
2615
2647
  ico = '↵'; catCls = 'cat-user';
2616
2648
  const t = (ev.text || '').trim();
2617
- if (t.length > 90) { lbl = esc(t.slice(0, 90) + '…'); lblTitle = esc(t); }
2618
- else lbl = esc(t);
2649
+ lbl = esc(t.length > 90 ? t.slice(0, 90) + '…' : t);
2619
2650
  }
2620
2651
 
2621
2652
  const errClass = ev._errored ? ' errored' : '';
2622
2653
  const selClass = selectedEntryId && selectedEntryId === id ? ' selected' : '';
2623
2654
 
2624
- const evData = JSON.stringify(ev).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
2655
+ const evData = JSON.stringify(ev).replace(/'/g, '&#39;');
2625
2656
  return `<div class="feed-entry k-${ev.kind}${errClass}${selClass}" data-ev='${evData}' onclick="openDetail(this.dataset.ev)">
2626
2657
  <div class="feed-ico ${catCls}">${ico}</div>
2627
2658
  <div class="feed-body">
2628
- <div class="feed-lbl"${lblTitle ? ` title="${lblTitle}"` : ''}>${lbl}</div>
2659
+ <div class="feed-lbl">${lbl}</div>
2629
2660
  ${detail ? `<div class="feed-detail">${detail}</div>` : ''}
2630
2661
  </div>
2631
- <div class="feed-ts"${tsTitle ? ` title="${tsTitle}"` : ''}>${ts}</div>
2662
+ <div class="feed-ts">${ts}</div>
2632
2663
  </div>`;
2633
2664
  }
2634
2665
 
@@ -2664,23 +2695,20 @@ function openDetail(evJson) {
2664
2695
 
2665
2696
  if (ev.kind === 'tool') {
2666
2697
  const { catCls } = toolStyle(ev.cat, ev.name);
2667
- const _toolId = (ev.id || '').toString();
2668
- const _toolLabel = ev.label || ev.name || '';
2669
2698
  title = ev.name || 'Tool';
2670
2699
  bodyHtml = `
2671
2700
  <div class="d-cat-pill ${catCls}" style="font-size:11px">${esc(ev.cat || 'other')}</div>
2672
- <div class="d-row"><div class="d-lbl">Label</div><div class="d-val" style="display:flex;align-items:center;gap:6px"><span>${esc(_toolLabel)}</span>${_toolLabel ? `<button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 5px" title="Copy label" onclick="navigator.clipboard.writeText(${JSON.stringify(_toolLabel).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Label copied','ok'))">⎘</button>` : ''}</div></div>
2701
+ <div class="d-row"><div class="d-lbl">Label</div><div class="d-val">${esc(ev.label || ev.name)}</div></div>
2673
2702
  ${ev.subagent ? `<div class="d-row"><div class="d-lbl">Subagent</div><div class="d-val">${esc(ev.subagent)}</div></div>` : ''}
2674
2703
  ${ev._errored ? `<div class="d-row"><div class="d-lbl">Status</div><div class="d-val error">Error</div></div>` : ''}
2675
- <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '—'}</div></div>
2676
- <div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val" style="display:flex;align-items:center;gap:6px"><span class="mono" title="${esc(_toolId)}">${esc(_toolId.slice(0, 24))}${_toolId.length > 24 ? '' : ''}</span>${_toolId ? `<button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 5px" title="Copy tool ID" onclick="navigator.clipboard.writeText(${JSON.stringify(_toolId).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Tool ID copied','ok'))">⎘</button>` : ''}</div></div>
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>
2677
2706
  `;
2678
2707
  } else if (ev.kind === 'user') {
2679
- const _userText = ev.text || '';
2680
2708
  title = 'User message';
2681
2709
  bodyHtml = `
2682
- <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '—'}</div></div>
2683
- <div class="d-row"><div class="d-lbl">Message</div><div class="d-val"><div style="white-space:pre-wrap;margin-bottom:6px">${esc(_userText)}</div>${_userText ? `<button class="btn" style="font-size:10px;padding:1px 6px" title="Copy message text" onclick="navigator.clipboard.writeText(${JSON.stringify(_userText).replace(/"/g, '&quot;')}).then(()=>showToast('Copied','Message copied','ok'))">⎘ Copy</button>` : ''}</div></div>
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>
2684
2712
  `;
2685
2713
  }
2686
2714
 
@@ -2708,10 +2736,10 @@ function buildSparkline() {
2708
2736
  if (idx >= 0 && idx < DAYS) buckets[idx]++;
2709
2737
  }
2710
2738
  const max = Math.max(...buckets, 1);
2711
- // pad so first column starts on Monday: compute day-of-week for day 0 (83 days ago)
2712
- const firstDow = new Date(now - (DAYS - 1) * DAY).getDay(); // 0=Sun
2713
- const startOffset = firstDow === 0 ? 6 : firstDow - 1; // Mon=0 offset
2714
- const padCells = Array.from({ length: startOffset }, () => '<div class="cal-cell" style="opacity:0"></div>');
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;
2715
2743
  const cells = buckets.map((v, i) => {
2716
2744
  const isToday = i === DAYS - 1;
2717
2745
  const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
@@ -2720,7 +2748,7 @@ function buildSparkline() {
2720
2748
  const title = `${label}: ${v} session${v !== 1 ? 's' : ''}`;
2721
2749
  return `<div class="cal-cell cal-${level}${isToday ? ' cal-today' : ''}" title="${title}"></div>`;
2722
2750
  });
2723
- return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${padCells.join('')}${cells.join('')}</div></div>`;
2751
+ return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${cells.join('')}</div></div>`;
2724
2752
  }
2725
2753
 
2726
2754
  // ── alerts rail ────────────────────────────────────────────
@@ -2750,10 +2778,6 @@ function updateAlerts() {
2750
2778
  all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
2751
2779
  }
2752
2780
 
2753
- for (const l of (alertState.hilLoops || [])) {
2754
- all.push({ id: 'hil-' + l, cls: 'alert-warn', ico: '⚠', msg: `Loop waiting for response: ${l}`, action: `switchView('loops')` });
2755
- }
2756
-
2757
2781
  const visible = all.filter(a => !dismissedAlerts.has(a.id));
2758
2782
  if (!visible.length) {
2759
2783
  rail.className = '';
@@ -2763,7 +2787,7 @@ function updateAlerts() {
2763
2787
  rail.className = 'has-alerts';
2764
2788
  rail.innerHTML = visible.map(a =>
2765
2789
  `<div class="alert-item ${a.cls}" data-alert-id="${a.id}"${a.action ? ` onclick="${a.action}" style="cursor:pointer"` : ''}>
2766
- <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>
2767
2791
  </div>`).join('');
2768
2792
  }
2769
2793
 
@@ -2795,12 +2819,7 @@ function showSessCtx(sess) {
2795
2819
  bar.classList.remove('show');
2796
2820
  return;
2797
2821
  }
2798
- const sCtxAge = sess.lastTs || sess.mtime;
2799
- const sCtxTime = sCtxAge ? ' · ' + relTime(sCtxAge) : '';
2800
- const sCtxText = sess.lastPrompt || sess.id.slice(0, 16) + '…';
2801
- const label = document.getElementById('sctx-label');
2802
- label.textContent = sCtxText + sCtxTime;
2803
- label.title = sCtxText + (sCtxAge ? ' · ' + new Date(typeof sCtxAge === 'number' ? sCtxAge : Number(sCtxAge) || sCtxAge).toLocaleString() : '');
2822
+ document.getElementById('sctx-label').textContent = sess.lastPrompt || sess.id.slice(0, 16) + '…';
2804
2823
  bar.classList.add('show');
2805
2824
  }
2806
2825
 
@@ -2822,7 +2841,6 @@ async function loadTodayMetrics() {
2822
2841
  const data = await apiFetch('/api/section?name=tokens&dir=' + enc(DIR));
2823
2842
  const s = data?.tokens?.summary || {};
2824
2843
  alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
2825
- alertState.monthCost = typeof s.monthCost === 'number' ? s.monthCost : 0;
2826
2844
  updateAlerts();
2827
2845
  checkBudget();
2828
2846
  // topbar cost badge
@@ -2859,17 +2877,13 @@ async function loadLoopMetrics() {
2859
2877
  try {
2860
2878
  const data = await apiFetch('/api/loops?dir=' + enc(DIR));
2861
2879
  const loops = Array.isArray(data) ? data : (data.loops || []);
2862
- const hilCount = loops.filter(l => l.status === 'hil:pending').length;
2863
- document.getElementById('bdg-loops').textContent = loops.length ? (hilCount ? loops.length + '⚠' : loops.length) : '—';
2880
+ document.getElementById('bdg-loops').textContent = loops.length || '';
2864
2881
 
2865
2882
  // alert on loops running > 2h
2866
2883
  const TWO_HOURS = 2 * 3600 * 1000;
2867
2884
  const now = Date.now();
2868
2885
  alertState.longLoops = loops
2869
- .filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.status !== 'hil:pending' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
2870
- .map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
2871
- alertState.hilLoops = loops
2872
- .filter(l => l.status === 'hil:pending')
2886
+ .filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
2873
2887
  .map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
2874
2888
  updateAlerts();
2875
2889
 
@@ -2878,24 +2892,13 @@ async function loadLoopMetrics() {
2878
2892
  return;
2879
2893
  }
2880
2894
  const items = loops.slice(0, 5).map(l => {
2881
- const fullName = (l.name || l.prompt || 'loop').split('--')[0].trim();
2882
- const name = fullName.slice(0, 36);
2883
- const isHilMini = l.status === 'hil:pending';
2884
- const isTillendMini = l.type === 'tillend';
2885
- const intervalMini = fmtInterval(l.interval || l.schedule) || 'running';
2886
- const repMini = isTillendMini && l.currentRep ? `run ${l.currentRep}${l.maxReps ? '/' + l.maxReps : ''}` : null;
2887
- const hilDot = isHilMini ? ' <span style="color:oklch(75% 0.16 60);font-size:9px">⚠HIL</span>' : '';
2888
- const typeDot = isTillendMini ? '<span style="color:oklch(70% 0.18 280);font-size:9px;margin-right:3px">∞</span>' : '';
2895
+ const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 36);
2889
2896
  return `<div class="mini-loop">
2890
- <div class="ml-name" title="${esc(fullName)}">${typeDot}${esc(name)}${hilDot}</div>
2891
- <div class="ml-meta"><span class="ml-dot"></span>${esc(repMini || intervalMini)}</div>
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>
2892
2899
  </div>`;
2893
2900
  }).join('');
2894
- const overflow = loops.length > 5 ? loops.length - 5 : 0;
2895
- const overflowNote = overflow > 0
2896
- ? `<div style="font-size:10px;color:var(--text-xs);padding:3px 0 0">+${overflow} more — open Loops tab</div>`
2897
- : '';
2898
- document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}${overflowNote}`;
2901
+ document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}`;
2899
2902
  } catch (_) {
2900
2903
  document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt">—</div>`;
2901
2904
  }
@@ -2927,16 +2930,16 @@ async function loadStatusStrip() {
2927
2930
 
2928
2931
  // HNSW status
2929
2932
  const hnswOn = mem.hnsw === true || mem.hnswEnabled === true || mem.hnsw_enabled === true;
2930
- pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}" title="Hierarchical Navigable Small World index — fast approximate nearest-neighbour memory search">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
2933
+ pills.push(`<span class="ss-pill ${hnswOn ? 'on' : ''}">HNSW ${hnswOn ? 'ON' : 'OFF'}</span>`);
2931
2934
 
2932
2935
  // Patterns count
2933
2936
  if (mem.patterns != null) {
2934
- 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>`);
2935
2938
  }
2936
2939
 
2937
2940
  // Chunks count
2938
2941
  if (mem.chunks != null) {
2939
- 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>`);
2940
2943
  }
2941
2944
 
2942
2945
  // Swarm status
@@ -2968,7 +2971,7 @@ async function loadTokensView() {
2968
2971
  const rows = Array.isArray(data?.tokens?.rows) ? data.tokens.rows : [];
2969
2972
  cards.innerHTML = [
2970
2973
  { label:'Today Cost', val: typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—' },
2971
- { label:'Today Calls', val: s.todayCalls != null ? Number(s.todayCalls).toLocaleString() : '—' },
2974
+ { label:'Today Calls', val: s.todayCalls ?? '—' },
2972
2975
  { label:'Month Cost', val: typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—' },
2973
2976
  { label:'Total Tokens', val: s.totalTokens != null ? Number(s.totalTokens).toLocaleString() : '—' },
2974
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('');
@@ -2980,13 +2983,12 @@ async function loadTokensView() {
2980
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>' +
2981
2984
  '</tr></thead><tbody>' +
2982
2985
  rows.slice(0, 30).map(r => `<tr style="border-top:1px solid var(--border)">
2983
- <td style="padding:4px 8px 4px 0;color:var(--text-hi);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.session || r.id || '')}">${esc(r.session || r.id || '—')}</td>
2984
- <td style="padding:4px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>
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>
2985
2988
  <td style="padding:4px 8px;color:var(--text-lo)">${r.tokens != null ? Number(r.tokens).toLocaleString() : '—'}</td>
2986
2989
  <td style="padding:4px 8px;color:var(--accent)">$${Number(r.cost ?? 0).toFixed(4)}</td>
2987
2990
  </tr>`).join('') +
2988
- '</tbody></table></div>' +
2989
- (rows.length > 30 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:6px;text-align:right">Showing 30 of ${rows.length} sessions</div>` : '');
2991
+ '</tbody></table></div>';
2990
2992
  } else { table.innerHTML = ''; }
2991
2993
  markLiveGlow('view-tokens');
2992
2994
  } catch (_) {
@@ -3038,6 +3040,7 @@ function renderTokChart(daily, animated = true) {
3038
3040
  });
3039
3041
  canvas.addEventListener('mouseleave', () => { canvas._tokTip.style.display = 'none'; });
3040
3042
  }
3043
+
3041
3044
  const targets = vals.map((v, i) => ({
3042
3045
  v, i,
3043
3046
  isToday: i === vals.length - 1,
@@ -3098,7 +3101,7 @@ async function setTokPeriod(btn, period) {
3098
3101
  if (cards) cards.innerHTML = [
3099
3102
  { label: 'Cost', val: typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2)
3100
3103
  : typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '—' },
3101
- { label: 'Calls', val: (s.todayCalls ?? s.calls) != null ? Number(s.todayCalls ?? s.calls).toLocaleString() : '—' },
3104
+ { label: 'Calls', val: s.todayCalls ?? s.calls ?? '—' },
3102
3105
  { label: 'Tokens', val: s.totalTokens != null ? Number(s.totalTokens).toLocaleString() : '—' },
3103
3106
  { label: 'Models', val: s.modelCount ?? s.models ?? '—' },
3104
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('');
@@ -3110,13 +3113,12 @@ async function setTokPeriod(btn, period) {
3110
3113
  '<th style="padding:3px 8px">Calls</th><th style="padding:3px 8px">Cost</th></tr></thead><tbody>' +
3111
3114
  rows.slice(0, 30).map(r =>
3112
3115
  `<tr style="border-top:1px solid var(--border)">` +
3113
- `<td style="padding:3px 8px 3px 0;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.session || r.label || r.id || '')}">${esc(r.session || r.label || r.id || '—')}</td>` +
3114
- `<td style="padding:3px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>` +
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>` +
3115
3118
  `<td style="padding:3px 8px;color:var(--accent)">$${Number(r.cost ?? 0).toFixed(4)}</td>` +
3116
3119
  `</tr>`
3117
3120
  ).join('') +
3118
- '</tbody></table></div>' +
3119
- (rows.length > 30 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:6px;text-align:right">Showing 30 of ${rows.length} entries</div>` : '');
3121
+ '</tbody></table></div>';
3120
3122
  } else if (table) { table.innerHTML = ''; }
3121
3123
  markLiveGlow('view-tokens');
3122
3124
  // Update topbar badge when showing today's data
@@ -3169,58 +3171,25 @@ async function loadMemRouting() {
3169
3171
  const last = rows[rows.length - 1];
3170
3172
  window._lastRouteAgent = last.suggestedAgent || last.route || last.category || last.agent || last.agentType || '';
3171
3173
  }
3172
- if (!rows.length) { pane.innerHTML = '<div class="empty">No routing data<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Routing data accumulates as agents run tasks. Train the router with <code>npx monomind hooks route --task "…"</code></div></div>'; return; }
3173
- const displayRows = rows.slice(-40).reverse();
3174
- const overflowNote = rows.length > 40 ? `<div style="font-size:11px;color:var(--text-xs);margin-bottom:6px;text-align:right">Showing last 40 of ${rows.length} entries</div>` : '';
3175
- pane.innerHTML = '<div class="filter-bar" style="margin-bottom:8px"><input class="filter-input" id="routing-filter" type="text" placeholder="Filter by agent or task…" oninput="filterRouting(this.value)" title="Filter routing entries"></div>' +
3176
- overflowNote +
3177
- '<div id="routing-rows">' +
3178
- displayRows.map(r => {
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 => {
3179
3177
  const agent = r.suggestedAgent || r.route || r.category || r.agent || '—';
3180
3178
  const task = r.task || r.prompt || r.description || r.sessionId?.slice(0, 8) || '—';
3181
3179
  const ts = r.timestamp || r.ts || r.created_at;
3182
3180
  const conf = r.confidence != null ? Math.round(r.confidence * 100) + '%' : '';
3183
- return `<div class="routing-entry" style="padding:5px 0;border-bottom:1px solid var(--border);font-size:11px;font-family:monospace" data-agent="${esc(agent)}" data-task="${esc(task)}">
3181
+ return `<div style="padding:5px 0;border-bottom:1px solid var(--border);font-size:11px;font-family:monospace">
3184
3182
  <div style="color:var(--text-hi);display:flex;align-items:center;gap:8px">
3185
3183
  <span style="color:var(--accent)">${esc(agent)}</span>
3186
3184
  ${conf ? `<span style="color:var(--text-lo)">${esc(conf)}</span>` : ''}
3187
- <span style="color:var(--text-lo);margin-left:auto;font-size:10px" title="${ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : ''}">${relTime(ts)}</span>
3185
+ <span style="color:var(--text-lo);margin-left:auto;font-size:10px">${relTime(ts)}</span>
3188
3186
  </div>
3189
3187
  <div style="color:var(--text-lo);margin-top:1px">${esc(task)}</div>
3190
3188
  </div>`;
3191
- }).join('') + '</div>';
3189
+ }).join('');
3192
3190
  } catch (_) { pane.innerHTML = '<div class="empty">Failed to load routing data</div>'; }
3193
3191
  }
3194
3192
 
3195
- function filterRouting(q) {
3196
- const lq = (q || '').toLowerCase();
3197
- document.querySelectorAll('#routing-rows .routing-entry').forEach(el => {
3198
- const agent = (el.dataset.agent || '').toLowerCase();
3199
- const task = (el.dataset.task || '').toLowerCase();
3200
- el.style.display = (!lq || agent.includes(lq) || task.includes(lq)) ? '' : 'none';
3201
- });
3202
- }
3203
-
3204
- function filterLoopList(q) {
3205
- const lq = (q || '').toLowerCase();
3206
- document.querySelectorAll('#loops-content .loop-row').forEach(el => {
3207
- const text = (el.textContent || '').toLowerCase();
3208
- const expand = el.nextElementSibling;
3209
- const visible = !lq || text.includes(lq);
3210
- el.style.display = visible ? '' : 'none';
3211
- if (expand && expand.classList.contains('loop-expand')) expand.style.display = 'none';
3212
- });
3213
- }
3214
-
3215
- function filterOrgList(q) {
3216
- const lq = (q || '').toLowerCase();
3217
- document.querySelectorAll('#orgs-list-scroll .org-item').forEach(el => {
3218
- const name = (el.dataset.org || '').toLowerCase();
3219
- const goal = (el.querySelector('.oi-goal')?.textContent || '').toLowerCase();
3220
- el.style.display = (!lq || name.includes(lq) || goal.includes(lq)) ? '' : 'none';
3221
- });
3222
- }
3223
-
3224
3193
  async function loadMemUsage() {
3225
3194
  const pane = document.getElementById('mem-tab-usage');
3226
3195
  if (!pane) return;
@@ -3228,10 +3197,10 @@ async function loadMemUsage() {
3228
3197
  // Period tabs
3229
3198
  pane.innerHTML = `
3230
3199
  <div class="tok-periods" style="margin-bottom:14px">
3231
- <button class="tok-period-btn active" data-period="today" title="Show today's memory usage" onclick="loadMemUsagePeriod(this,'today')">Today</button>
3232
- <button class="tok-period-btn" data-period="week" title="Show this week's memory usage" onclick="loadMemUsagePeriod(this,'week')">Week</button>
3233
- <button class="tok-period-btn" data-period="30d" title="Show last 30 days of memory usage" onclick="loadMemUsagePeriod(this,'30d')">30 Days</button>
3234
- <button class="tok-period-btn" data-period="month" title="Show this month's memory usage" onclick="loadMemUsagePeriod(this,'month')">Month</button>
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>
3235
3204
  </div>
3236
3205
  <div id="mem-usage-content"><div class="loading-txt">Loading…</div></div>
3237
3206
  `;
@@ -3256,25 +3225,19 @@ async function loadMemUsagePeriod(btn, period) {
3256
3225
 
3257
3226
  function barChart(items, valKey, labelKey, color, maxItems) {
3258
3227
  if (!items.length) return '<div class="empty" style="font-size:12px">No data</div>';
3259
- const shown = items.slice(0, maxItems);
3260
- const maxVal = Math.max(...shown.map(x => Number(x[valKey] || 0)), 0.0001);
3261
- const rows = shown.map(item => {
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 => {
3262
3230
  const pct = Math.round((Number(item[valKey] || 0) / maxVal) * 100);
3263
- const fullLabel = String(item[labelKey] || '—');
3264
- const label = esc(fullLabel.slice(0, 24));
3231
+ const label = esc(String(item[labelKey] || '—').slice(0, 24));
3265
3232
  const val = typeof item[valKey] === 'number' && valKey === 'cost'
3266
3233
  ? '$' + Number(item[valKey]).toFixed(4)
3267
3234
  : String(item[valKey] || 0);
3268
3235
  return `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">
3269
- <div style="width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc(fullLabel)}">${label}${fullLabel.length > 24 ? '…' : ''}</div>
3270
- <div style="flex:1;height:7px;background:var(--border);border-radius:2px;overflow:hidden" title="${esc(fullLabel)}: ${esc(val)}"><div style="width:${pct}%;height:100%;background:${color};border-radius:2px"></div></div>
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>
3271
3238
  <div style="width:60px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">${esc(val)}</div>
3272
3239
  </div>`;
3273
3240
  }).join('');
3274
- const overflow = items.length > maxItems
3275
- ? `<div style="font-size:11px;color:var(--text-xs);margin-top:3px;text-align:right">Showing ${maxItems} of ${items.length}</div>`
3276
- : '';
3277
- return rows + overflow;
3278
3241
  }
3279
3242
 
3280
3243
  const totalCost = typeof s.todayCost === 'number' ? s.todayCost : (typeof s.cost === 'number' ? s.cost : null);
@@ -3317,12 +3280,11 @@ async function loadMemUsagePeriod(btn, period) {
3317
3280
  <div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:11px">
3318
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>
3319
3282
  <tbody>${rows.slice(0,30).map(r => `<tr style="border-top:1px solid var(--border)">
3320
- <td style="padding:3px 8px 3px 0;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:11px" title="${esc(r.session||r.label||r.id||'')}">${esc(r.session||r.label||r.id||'—')}</td>
3321
- <td style="padding:3px 8px;color:var(--text-lo)">${r.calls != null ? Number(r.calls).toLocaleString() : '—'}</td>
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>
3322
3285
  <td style="padding:3px 8px;color:var(--accent)">$${Number(r.cost??0).toFixed(4)}</td>
3323
3286
  </tr>`).join('')}</tbody>
3324
- </table></div>
3325
- ${rows.length > 30 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:6px;text-align:right">Showing 30 of ${rows.length} entries</div>` : ''}` : ''}
3287
+ </table></div>` : ''}
3326
3288
  `;
3327
3289
  } catch (e) {
3328
3290
  content.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>';
@@ -3344,15 +3306,14 @@ async function loadMemADRs() {
3344
3306
  function renderADRs(list) {
3345
3307
  const pane = document.getElementById('adr-content');
3346
3308
  if (!pane) return;
3347
- if (!list.length) { pane.innerHTML = '<div class="empty">No ADRs found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">ADRs are created by agents via <code>monomind memory store --type adr</code></div></div>'; return; }
3309
+ if (!list.length) { pane.innerHTML = '<div class="empty">No ADRs found</div>'; return; }
3348
3310
  pane.innerHTML = list.slice(0, 50).map(a => `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
3349
3311
  <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px">
3350
3312
  <span style="color:var(--text-hi);font-size:12px;font-family:monospace">${esc(a.id || a.title || '—')}</span>
3351
3313
  <span class="ss-pill ${a.status === 'accepted' ? 'on' : a.status === 'deprecated' ? 'warn' : ''}">${esc(a.status || '?')}</span>
3352
3314
  </div>
3353
- <div style="font-size:11px;color:var(--text-lo)" title="${esc(a.context || a.summary || '')}">${esc((a.context || a.summary || '').slice(0, 120))}${(a.context || a.summary || '').length > 120 ? '…' : ''}</div>
3354
- </div>`).join('') +
3355
- (list.length > 50 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:8px;text-align:right">Showing 50 of ${list.length} ADRs</div>` : '');
3315
+ <div style="font-size:11px;color:var(--text-lo)">${esc((a.context || a.summary || '').slice(0, 120))}</div>
3316
+ </div>`).join('');
3356
3317
  }
3357
3318
 
3358
3319
  function filterADRs(q) {
@@ -3367,15 +3328,11 @@ function filterADRs(q) {
3367
3328
 
3368
3329
  function renderMiniSessions(sessions) {
3369
3330
  if (!sessions.length) return;
3370
- const items = sessions.map((s, i) => {
3371
- const costStr = typeof s.totalCost === 'number' && s.totalCost > 0.001 ? ' · $' + s.totalCost.toFixed(2) : '';
3372
- const durStr = s.totalDurationMs ? ' · ' + fmtDur(s.totalDurationMs) : '';
3373
- return `
3331
+ const items = sessions.map((s, i) => `
3374
3332
  <div class="mini-sess" onclick="sessionIdx=${i};userScrolled=false;loadFeedForSession(allSessions[${i}])">
3375
- <div class="ms-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
3376
- <div class="ms-meta"><span title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(s.lastTs || s.mtime)}">${relTime(s.lastTs || s.mtime)}</span>${durStr}${costStr}</div>
3377
- </div>`;
3378
- }).join('');
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('');
3379
3336
  document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
3380
3337
  buildSwimlane();
3381
3338
  }
@@ -3402,7 +3359,7 @@ function renderProjectGrid(projects, query) {
3402
3359
  (p.name || p.slug || '').toLowerCase().includes(query.toLowerCase()) ||
3403
3360
  (p.path || '').toLowerCase().includes(query.toLowerCase())) : projects;
3404
3361
  if (!filtered.length) {
3405
- el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Try a different search term or clear the filter</div></div>';
3362
+ el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div></div>';
3406
3363
  return;
3407
3364
  }
3408
3365
  el.className = 'proj-grid';
@@ -3413,12 +3370,12 @@ function renderProjectGrid(projects, query) {
3413
3370
  return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
3414
3371
  ${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
3415
3372
  <div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
3416
- <div class="proj-card-name" title="${esc(p.name || p.slug || '')}">${esc(p.name || p.slug)}</div>
3417
- <div class="proj-card-path" title="${esc(p.path || '')}">${esc(shortPath(p.path || ''))}</div>
3373
+ <div class="proj-card-name">${esc(p.name || p.slug)}</div>
3374
+ <div class="proj-card-path">${esc(p.path || '')}</div>
3418
3375
  <div class="proj-card-stats">
3419
3376
  <div class="proj-stat"><div class="ps-v">${p.sessionCount || 0}</div><div class="ps-l">sessions</div></div>
3420
3377
  <div class="proj-stat"><div class="ps-v">${p.memoryCount || 0}</div><div class="ps-l">memories</div></div>
3421
- ${p.lastActivity ? `<div class="proj-stat"><div class="ps-v" style="font-size:12px" title="${new Date(typeof p.lastActivity === 'number' ? p.lastActivity : Number(p.lastActivity) || p.lastActivity).toLocaleString()}">${relTime(p.lastActivity)}</div><div class="ps-l">last active</div></div>` : ''}
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>` : ''}
3422
3379
  </div>
3423
3380
  </div>`;
3424
3381
  }).join('');
@@ -3440,33 +3397,19 @@ async function renderSessions() {
3440
3397
  document.getElementById('sess-pg-sub').textContent =
3441
3398
  sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
3442
3399
  if (!sessions.length) {
3443
- el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Start a session with <code>npx monomind agent spawn</code></div></div>';
3400
+ el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
3444
3401
  return;
3445
3402
  }
3446
3403
  let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
3447
3404
  if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
3448
3405
  if (heatmapDateFilter) toShow = toShow.filter(s => {
3449
3406
  const t = s.lastTs || s.mtime; if (!t) return false;
3450
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString() === heatmapDateFilter;
3407
+ return new Date(typeof t === 'number' ? t : t).toDateString() === heatmapDateFilter;
3451
3408
  });
3452
3409
  // f57: file pivot filter
3453
3410
  if (filePivot) toShow = toShow.filter(s => (s.filesTouched || []).includes(filePivot));
3454
3411
  if (!toShow.length) {
3455
- let emptyMsg, emptyHint;
3456
- if (filePivot) {
3457
- emptyMsg = 'No sessions touching ' + esc(filePivot.split('/').pop());
3458
- emptyHint = 'Clear the file filter to see all sessions';
3459
- } else if (heatmapDateFilter) {
3460
- emptyMsg = 'No sessions on ' + esc(heatmapDateFilter);
3461
- emptyHint = 'Click another date on the heatmap or clear the filter';
3462
- } else if (activeTagFilter) {
3463
- emptyMsg = 'No sessions tagged "' + esc(activeTagFilter) + '"';
3464
- emptyHint = 'Clear the tag filter to see all sessions';
3465
- } else {
3466
- emptyMsg = 'No bookmarked sessions';
3467
- emptyHint = 'Click the ☆ on any session row to bookmark it.';
3468
- }
3469
- el.innerHTML = `<div class="empty"><div class="empty-ico">☆</div><div>${emptyMsg}</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">${emptyHint}</div></div>`;
3412
+ el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
3470
3413
  buildSessionHeatmap(sessions);
3471
3414
  return;
3472
3415
  }
@@ -3513,7 +3456,7 @@ async function renderSessions() {
3513
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('');
3514
3457
  const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
3515
3458
  const isStarred = bookmarks.has(s.id);
3516
- const sData = JSON.stringify(s).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/'/g, '&#39;');
3459
+ const sData = JSON.stringify(s).replace(/'/g, '&#39;');
3517
3460
  const note = getSessNote(s.id);
3518
3461
  const hasNote = !!note;
3519
3462
  const files = (s.filesTouched || []).slice(0, 5);
@@ -3538,14 +3481,12 @@ async function renderSessions() {
3538
3481
  ? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
3539
3482
  : '';
3540
3483
  const summaryHtml = s.summary
3541
- ? `<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>`
3542
3485
  : '';
3543
- const srTimeTs = s.lastTs || s.mtime;
3544
- const srTimeFull = srTimeTs ? new Date(typeof srTimeTs === 'number' ? srTimeTs : Number(srTimeTs) || srTimeTs).toLocaleString() : '';
3545
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}'>
3546
3487
  <div class="sr-top">
3547
- <div class="sr-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3548
- <div class="sr-time"${srTimeFull ? ` title="${esc(srTimeFull)}"` : ''}>${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
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}
3549
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>
3550
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>
3551
3492
  <span class="sr-view">→ view</span>
@@ -3559,7 +3500,7 @@ async function renderSessions() {
3559
3500
  ${ctxGauge}
3560
3501
  <div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
3561
3502
  <div class="sess-notes-wrap" onclick="event.stopPropagation()">
3562
- <button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" title="${hasNote ? 'Show/hide session note' : 'Add a note to this session'}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
3503
+ <button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
3563
3504
  <div class="sess-notes-area" id="snote-${esc(s.id)}">
3564
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>
3565
3506
  <div class="sess-note-saved"></div>
@@ -3600,14 +3541,14 @@ async function renderSessions() {
3600
3541
  const todayCost = allSessions.filter(s => {
3601
3542
  const t = s.firstTs || s.mtime;
3602
3543
  if (!t) return false;
3603
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
3544
+ const d = new Date(typeof t === 'number' ? t : t);
3604
3545
  const now = new Date();
3605
3546
  return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
3606
3547
  }).reduce((a, s) => a + (s.totalCost || 0), 0);
3607
3548
  const monthCost = allSessions.filter(s => {
3608
3549
  const t = s.firstTs || s.mtime;
3609
3550
  if (!t) return false;
3610
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
3551
+ const d = new Date(typeof t === 'number' ? t : t);
3611
3552
  const now = new Date();
3612
3553
  return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
3613
3554
  }).reduce((a, s) => a + (s.totalCost || 0), 0);
@@ -3686,7 +3627,7 @@ function buildTagFilterBar(sessions) {
3686
3627
  if (!allTags.common.size) return '';
3687
3628
  const sorted = [...allTags.common].sort();
3688
3629
  const chips = sorted.map(t =>
3689
- `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" title="Filter sessions by tag: ${esc(t)}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
3630
+ `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
3690
3631
  ).join('');
3691
3632
  return `<div class="tag-filter-bar">${chips}</div>`;
3692
3633
  }
@@ -3715,12 +3656,10 @@ function buildRecap(events, sess) {
3715
3656
  const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
3716
3657
 
3717
3658
  const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
3718
- const durStr = sess?.totalDurationMs ? fmtDur(sess.totalDurationMs) : null;
3719
3659
 
3720
3660
  const stats = [
3721
3661
  tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
3722
3662
  users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
3723
- durStr ? `<span class="recap-stat">${durStr}</span>` : '',
3724
3663
  costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
3725
3664
  errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
3726
3665
  ].filter(Boolean).join('');
@@ -3729,74 +3668,6 @@ function buildRecap(events, sess) {
3729
3668
  recap.className = 'show';
3730
3669
  }
3731
3670
 
3732
- // ── feature 3: global feed ─────────────────────────────────
3733
- async function renderGlobalFeed() {
3734
- const el = document.getElementById('gf-content');
3735
- el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
3736
- try {
3737
- // fetch project list
3738
- const data = await apiFetch('/api/projects');
3739
- const allProjects = data?.projects || [];
3740
- const projects = allProjects.slice(0, 8);
3741
- if (!projects.length) {
3742
- el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Run <code>npx monomind init</code> inside a project to register it.</div></div>';
3743
- return;
3744
- }
3745
- document.getElementById('gf-sub').textContent = allProjects.length > 8
3746
- ? `Last activity across ${projects.length} of ${allProjects.length} projects`
3747
- : `Last activity across ${projects.length} project${projects.length !== 1 ? 's' : ''}`;
3748
-
3749
- // fetch sessions for each project in parallel
3750
- const results = await Promise.allSettled(
3751
- projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
3752
- );
3753
-
3754
- // flatten + sort by recency
3755
- const entries = [];
3756
- for (const r of results) {
3757
- if (r.status !== 'fulfilled') continue;
3758
- const { project, sessions } = r.value;
3759
- for (const s of sessions.slice(0, 3)) {
3760
- entries.push({ project, session: s });
3761
- }
3762
- }
3763
- entries.sort((a, b) => {
3764
- const ta = a.session.lastTs || a.session.mtime || 0;
3765
- const tb = b.session.lastTs || b.session.mtime || 0;
3766
- return (typeof tb === 'number' ? tb : Number(tb) || new Date(tb).getTime() || 0) - (typeof ta === 'number' ? ta : Number(ta) || new Date(ta).getTime() || 0);
3767
- });
3768
-
3769
- if (!entries.length) {
3770
- el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Sessions appear when Claude Code runs inside registered projects</div></div>';
3771
- return;
3772
- }
3773
-
3774
- el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
3775
- const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
3776
- const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
3777
- const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
3778
- const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
3779
- const gfCompactBadge = (s.compactCount > 0)
3780
- ? `<span class="sr-compact-badge">+${s.compactCount} compacted</span>`
3781
- : '';
3782
- const gfSummaryHtml = s.summary
3783
- ? `<div class="sr-summary" title="${esc(s.summary)}">${esc(s.summary.slice(0, 180))}${s.summary.length > 180 ? '…' : ''}</div>`
3784
- : '';
3785
- return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
3786
- <div class="sr-top">
3787
- <div class="sr-prompt" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3788
- <div class="sr-time" title="${(t => t ? new Date(typeof t === 'number' ? t : Number(t) || t).toLocaleString() : '')(s.lastTs || s.mtime)}">${relTime(s.lastTs || s.mtime)}</div>${gfCompactBadge}
3789
- <span class="gf-proj-tag">${esc(projName)}</span>
3790
- </div>
3791
- ${gfSummaryHtml}
3792
- <div class="sr-meta">${esc(meta)}</div>
3793
- </div>`;
3794
- }).join('') + '</div>';
3795
- } catch (err) {
3796
- el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
3797
- }
3798
- }
3799
-
3800
3671
  // ── global loops (multi-project) ───────────────────────────
3801
3672
  function deduplicateLoops(loops) {
3802
3673
  const hasRepeat = loops.some(l => l.source === '_repeat.md');
@@ -3837,94 +3708,33 @@ async function renderGlobalLoops() {
3837
3708
  if (!loops.length) continue;
3838
3709
  totalLoops += loops.length;
3839
3710
  const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
3840
- const rows = loops.map((l, idx) => {
3711
+ const rows = loops.map(l => {
3841
3712
  const isTillend = l.type === 'tillend';
3842
3713
  const curRep = l.currentRep || 0;
3843
3714
  const maxReps = l.maxReps || 0;
3844
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
3715
+ const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
3845
3716
  const isHil = l.status === 'hil:pending';
3846
3717
  const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
3847
- const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
3848
- const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive &&
3849
- nextAt > 0 && nextAt <= Date.now();
3850
- const isStaledActive = isExplicitlyActive && nextAt > 0 &&
3851
- (Date.now() - nextAt) > LOOP_STALE_MS;
3852
- const isFinished = isOverdue || isStaledActive ||
3853
- (!isExplicitlyActive && maxReps > 0 && curRep >= maxReps) ||
3718
+ const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
3719
+ const isFinished = isOverdue || (maxReps > 0 && curRep >= maxReps) ||
3854
3720
  ['finished','done','complete','completed','expired'].includes(l.status);
3855
3721
  const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
3722
+ const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
3856
3723
  const intervalStr = fmtInterval(l.interval || l.schedule);
3857
- const _lp = (function(_l) {
3858
- if (_l.command) {
3859
- const flags = [];
3860
- if (_l.type && _l.type !== 'repeat') flags.push('--' + _l.type);
3861
- if (_l.maxReps) flags.push('--maxruns ' + _l.maxReps);
3862
- if (_l.wait || _l.interval) flags.push('--wait ' + (_l.wait || _l.interval * 60));
3863
- if (_l.currentRep != null) flags.push('--rep ' + _l.currentRep);
3864
- if (_l.id) flags.push('--loop ' + _l.id);
3865
- return { userPrompt: _l.prompt || '', command: _l.command, flagsStr: flags.join(' ') };
3866
- }
3867
- const full = _l.prompt || '';
3868
- const cmdM = full.match(/^(\/\S+)/);
3869
- if (!cmdM) return { userPrompt: full, command: '', flagsStr: '' };
3870
- const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
3871
- let ti = 0, fp = [];
3872
- while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
3873
- fp.push(tokens[ti++]);
3874
- if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) fp.push(tokens[ti++]);
3875
- }
3876
- return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1], flagsStr: fp.join(' ') };
3877
- })(l);
3878
- const userPrompt = _lp.userPrompt;
3879
- const cmdStr = _lp.command;
3880
- const flagsStr = _lp.flagsStr;
3881
- const fullPrompt = l.prompt || '';
3882
- const name = (l.name || userPrompt || cmdStr || 'loop').slice(0, 60);
3883
- const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
3884
- const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
3885
- const pct = (!isTillend && maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
3886
- const progBar = (!isTillend && maxReps > 0 && running)
3887
- ? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>` : '';
3888
- const runCountDisplay = isTillend
3889
- ? `run ${curRep} / ∞${maxReps > 0 ? ' (cap: ' + maxReps + ')' : ''}`
3890
- : (maxReps > 0 ? `${curRep} / ${maxReps}` : String(curRep || '—'));
3891
- const cdownSpan = nextAt
3892
- ? ` <span class="loop-cdown${nextAt - Date.now() <= 0 ? ' overdue' : ''}" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>` : '';
3893
- const stopBtn = running
3894
- ? `<button class="loop-stop-btn" data-loop-id="${esc(l.id || l.name || String(idx))}" onclick="stopLoop(event, this.dataset.loopId)" title="Stop this loop">■ Stop</button>` : '';
3895
- const typeBadge = `<span class="loop-type-badge${isTillend ? ' tillend' : ''}">${esc(l.type || 'repeat')}</span>`;
3724
+ const type = esc(l.type || 'repeat');
3896
3725
  const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
3897
3726
  const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
3898
- const hilBanner = isHil
3899
- ? `<div class="loop-hil-banner">⚠ Waiting for human response open HIL file to resume</div>` : '';
3900
- const metaParts = [intervalStr, l.description].filter(Boolean).join(' · ').slice(0, 80);
3901
- return `<div class="loop-row" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
3902
- <div class="loop-ico">${isTillend ? '∞' : '↺'}</div>
3903
- <div class="loop-body">
3904
- <div class="loop-name" title="${esc(userPrompt || fullPrompt)}">${typeBadge}${esc(name)}</div>
3905
- <div class="loop-meta">${esc(metaParts)}${cdownSpan}</div>
3906
- ${hilBanner}
3907
- ${progBar}
3908
- </div>
3909
- <div class="loop-status ${statusClass}">${statusLabel}</div>
3910
- ${stopBtn}
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>
3911
3734
  </div>
3912
- <div class="loop-expand">
3913
- ${userPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(userPrompt)}">${esc(userPrompt.slice(0, 300))}${userPrompt.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy prompt" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(userPrompt)}).then(()=>showToast('Copied','Prompt copied','ok'))">⎘</button></div></div>` : ''}
3914
- ${cmdStr ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="display:flex;align-items:center;gap:8px"><span>${esc(cmdStr)}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy command" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(cmdStr)}).then(()=>showToast('Copied','Command copied','ok'))">⎘</button></div></div>` : ''}
3915
- ${flagsStr ? `<div class="le-row"><div class="le-lbl">Flags</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(flagsStr)}">${esc(flagsStr.slice(0, 300))}${flagsStr.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy flags" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(flagsStr)}).then(()=>showToast('Copied','Flags copied','ok'))">⎘</button></div></div>` : ''}
3916
- <div class="le-row"><div class="le-lbl">Project</div><div class="le-val"><span class="gf-proj-tag">${esc(projName)}</span></div></div>
3917
- <div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${esc(l.type || 'repeat')}</div></div>
3918
- <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(intervalStr || '—')}</div></div>
3919
- <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isHil ? '⚠ hil:pending' : (running ? '● running' : (isFinished ? '✓ done' : '○ stopped'))}</div></div>
3920
- ${isHil && l.id ? `<div class="le-row"><div class="le-lbl">HIL file</div><div class="le-val mono" style="color:oklch(75% 0.16 60);word-break:break-all">.monomind/loops/${esc(l.id)}-hil.md</div></div>` : ''}
3921
- <div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
3922
- ${(()=>{ const sMs=l.startedAt?(typeof l.startedAt==='number'?l.startedAt:new Date(l.startedAt).getTime()):0; const age=sMs>0&&sMs<Date.now()?Date.now()-sMs:0; return age>0?`<div class="le-row"><div class="le-lbl">Running for</div><div class="le-val">${fmtDur(age)}</div></div>`:''; })()}
3923
- <div class="le-row"><div class="le-lbl">Last run</div><div class="le-val" title="${l.lastRunAt ? new Date(typeof l.lastRunAt === 'number' ? l.lastRunAt : Number(l.lastRunAt) || l.lastRunAt).toLocaleString() : ''}">${esc(lastRun)}</div></div>
3924
- <div class="le-row"><div class="le-lbl">${isTillend ? 'Progress' : 'Run count'}</div><div class="le-val">${esc(runCountDisplay)}</div></div>
3925
- ${l.source ? `<div class="le-row"><div class="le-lbl">Source</div><div class="le-val">${esc(l.source)}</div></div>` : ''}
3926
- ${buildLoopSparkline(l)}
3927
- </div>`;
3735
+ <span class="gf-proj-tag">${esc(projName)}</span>
3736
+ <div class="loop-status ${statusClass}">${statusLabel}</div>
3737
+ </div>`;
3928
3738
  }).join('');
3929
3739
  sections.push(`<div style="margin-bottom:18px">
3930
3740
  <div class="m-group-title" style="margin-bottom:6px">${esc(projName)}</div>
@@ -3940,7 +3750,6 @@ async function renderGlobalLoops() {
3940
3750
  return;
3941
3751
  }
3942
3752
  el.innerHTML = sections.join('');
3943
- startCountdowns();
3944
3753
  } catch (err) {
3945
3754
  el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
3946
3755
  }
@@ -3998,9 +3807,7 @@ async function renderGlobalTokens() {
3998
3807
  </tr>`).join('');
3999
3808
 
4000
3809
  const projectCount = rows.length;
4001
- document.getElementById('gt-sub').textContent = allProjects.length > 20
4002
- ? `Token usage across ${projectCount} of ${allProjects.length} projects`
4003
- : `Token usage across ${projectCount} project${projectCount !== 1 ? 's' : ''}`;
3810
+ document.getElementById('gt-sub').textContent = `Token usage across ${projectCount} project${projectCount !== 1 ? 's' : ''}`;
4004
3811
 
4005
3812
  el.innerHTML = `<div id="gt-cards" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px">${cards}</div>
4006
3813
  <div id="gt-table">
@@ -4021,6 +3828,217 @@ async function renderGlobalTokens() {
4021
3828
  }
4022
3829
  }
4023
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
+
4024
4042
  // ── feature 4: budget cap + desktop notification ───────────
4025
4043
  let budget = (function(){ try { return JSON.parse(localStorage.getItem('mm-budget') || '{}'); } catch { return {}; } })();
4026
4044
 
@@ -4045,7 +4063,6 @@ function saveBudget() {
4045
4063
  closeBudgetModal();
4046
4064
  checkBudget(); // check immediately
4047
4065
  updateBudgetBtnStyle();
4048
- showToast('Budget saved', budget.daily || budget.monthly ? `Daily: ${budget.daily ? '$'+budget.daily : '—'} · Monthly: ${budget.monthly ? '$'+budget.monthly : '—'}` : 'Budget cleared', 'ok');
4049
4066
  }
4050
4067
 
4051
4068
  function updateBudgetBtnStyle() {
@@ -4057,36 +4074,21 @@ function updateBudgetBtnStyle() {
4057
4074
 
4058
4075
  function checkBudget() {
4059
4076
  const cost = alertState.todayCost;
4060
- const moCost = alertState.monthCost;
4061
- // Daily budget check
4062
- if (budget.daily && cost) {
4077
+ if (!cost) return;
4078
+ if (budget.daily) {
4063
4079
  const pct = cost / budget.daily;
4064
4080
  if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
4065
4081
  alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
4066
4082
  alertState.budgetCls = 'alert-crit';
4067
- updateAlerts(); return;
4068
4083
  } else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
4069
4084
  alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
4070
4085
  alertState.budgetCls = 'alert-warn';
4071
4086
  maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
4072
- updateAlerts(); return;
4073
- }
4074
- }
4075
- // Monthly budget check
4076
- if (budget.monthly && moCost) {
4077
- const mpct = moCost / budget.monthly;
4078
- if (mpct >= 1 && !dismissedAlerts.has('budget-monthly-over')) {
4079
- alertState.budgetAlert = `Monthly budget exceeded: $${moCost.toFixed(2)} / $${budget.monthly}`;
4080
- alertState.budgetCls = 'alert-crit';
4081
- updateAlerts(); return;
4082
- } else if (mpct >= 0.8 && !dismissedAlerts.has('budget-monthly-warn')) {
4083
- alertState.budgetAlert = `Approaching monthly budget: $${moCost.toFixed(2)} / $${budget.monthly}`;
4084
- alertState.budgetCls = 'alert-warn';
4085
- updateAlerts(); return;
4087
+ } else {
4088
+ alertState.budgetAlert = null;
4086
4089
  }
4090
+ updateAlerts();
4087
4091
  }
4088
- alertState.budgetAlert = null;
4089
- updateAlerts();
4090
4092
  }
4091
4093
 
4092
4094
  function maybeNotify(title, body) {
@@ -4208,7 +4210,7 @@ function buildBreakdownByName(events) {
4208
4210
  const pct = Math.round(cnt / total * 100);
4209
4211
  return `<div class="tb-row">
4210
4212
  <div class="tb-lbl" style="width:54px" title="${esc(name)}">${esc(name.length > 8 ? name.slice(0,7)+'…' : name)}</div>
4211
- <div class="tb-bar-wrap" title="${esc(name)}: ${pct}% (${cnt} call${cnt!==1?'s':''})"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
4213
+ <div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
4212
4214
  <div class="tb-count">${cnt}</div>
4213
4215
  </div>`;
4214
4216
  }).join('');
@@ -4232,7 +4234,7 @@ function buildDigest() {
4232
4234
  const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
4233
4235
  const todaySessions = allSessions.filter(s => {
4234
4236
  const t = s.lastTs || s.mtime;
4235
- 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();
4236
4238
  });
4237
4239
  if (!todaySessions.length) return;
4238
4240
 
@@ -4251,8 +4253,8 @@ function buildDigest() {
4251
4253
  const stats = [
4252
4254
  `${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
4253
4255
  totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
4254
- totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
4255
- totalMsgs > 0 ? `${totalMsgs.toLocaleString()} messages` : null,
4256
+ totalTools > 0 ? `${totalTools} tool calls` : null,
4257
+ totalMsgs > 0 ? `${totalMsgs} messages` : null,
4256
4258
  longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
4257
4259
  ...themes.map(t => `#${t}`),
4258
4260
  ].filter(Boolean);
@@ -4264,7 +4266,7 @@ function buildDigest() {
4264
4266
  const monthCostSoFar = allSessions.filter(s => {
4265
4267
  const t = s.firstTs || s.mtime;
4266
4268
  if (!t) return false;
4267
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
4269
+ const d = new Date(typeof t === 'number' ? t : t);
4268
4270
  return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
4269
4271
  }).reduce((a, s) => a + (s.totalCost || 0), 0);
4270
4272
  const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
@@ -4333,25 +4335,23 @@ function toggleLeaderboard() {
4333
4335
  }
4334
4336
 
4335
4337
  function renderLeaderboard() {
4336
- const all = [...allSessions]
4337
- .filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
4338
- .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);
4339
4339
  const sorted = all.slice(0, 15);
4340
4340
  const body = document.getElementById('lb-body');
4341
- const overflow = document.getElementById('lb-overflow');
4342
- if (!sorted.length) { body.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-xs);padding:12px">No cost data yet</td></tr>'; if (overflow) overflow.textContent = ''; return; }
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; }
4343
4342
  body.innerHTML = sorted.map((s, i) => {
4344
4343
  const cost = '$' + s.totalCost.toFixed(2);
4345
4344
  const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
4346
4345
  const prompt = s.lastPrompt || s.id;
4347
4346
  return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
4348
4347
  <td class="lb-rank">${i + 1}</td>
4349
- <td class="lb-prompt">${esc(prompt.slice(0, 60))}${prompt.length > 60 ? '…' : ''}</td>
4348
+ <td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
4350
4349
  <td class="lb-cost">${cost}</td>
4351
4350
  <td class="lb-dur">${dur}</td>
4352
4351
  </tr>`;
4353
4352
  }).join('');
4354
- 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` : '';
4355
4355
  }
4356
4356
 
4357
4357
  // ── feature 12: session diff ──────────────────────────────
@@ -4468,7 +4468,7 @@ async function exportSession() {
4468
4468
  const events = data.events || [];
4469
4469
  const lines = [
4470
4470
  `# Session: ${sess.lastPrompt || sess.id}`,
4471
- `> ${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()}`,
4472
4472
  sess.totalCost != null ? `> Cost: $${sess.totalCost.toFixed(2)}` : '',
4473
4473
  sess.totalDurationMs ? `> Duration: ${fmtDur(sess.totalDurationMs)}` : '',
4474
4474
  '',
@@ -4476,7 +4476,7 @@ async function exportSession() {
4476
4476
  for (const ev of events) {
4477
4477
  if (ev.kind === 'user' && ev.text?.trim()) {
4478
4478
  lines.push(`\n## ${ev.text.trim().slice(0, 80)}`);
4479
- 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()}_`);
4480
4480
  } else if (ev.kind === 'tool') {
4481
4481
  const label = ev.label || ev.name || ev.cat;
4482
4482
  lines.push(`- \`${ev.name || ev.cat}\`${label ? ': ' + label : ''}${ev._errored ? ' ⚠ error' : ''}`);
@@ -4552,9 +4552,9 @@ function renderBurnGauge() {
4552
4552
  }
4553
4553
  const now = Date.now();
4554
4554
  // calls in last 5 min, 15 min, 60 min
4555
- const t5 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 300000).length;
4556
- const t15 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 900000).length;
4557
- const t60 = tools.filter(e => now - (typeof e.ts === 'number' ? e.ts : Number(e.ts) || 0) < 3600000).length;
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;
4558
4558
  const rate5 = (t5 / 5).toFixed(1); // calls/min
4559
4559
  const rate15 = (t15 / 15).toFixed(1);
4560
4560
  const rate60 = (t60 / 60).toFixed(1);
@@ -4583,7 +4583,7 @@ function buildSwimlane() {
4583
4583
  const LANE_HUES = [75, 200, 300, 150, 25, 220, 340, 120];
4584
4584
  const rows = recent.map((s, si) => {
4585
4585
  const start = s.firstTs || s.startTs || s.mtime || now;
4586
- const startMs = typeof start === 'number' ? start : Number(start) || new Date(start).getTime() || 0;
4586
+ const startMs = typeof start === 'number' ? start : new Date(start).getTime();
4587
4587
  const dur = s.totalDurationMs || 60000;
4588
4588
  const endMs = startMs + dur;
4589
4589
  const leftPct = Math.max(0, Math.min(100, ((startMs - windowStart) / windowMs) * 100));
@@ -4601,17 +4601,17 @@ function buildSwimlane() {
4601
4601
  }).join('');
4602
4602
  // dead time: find largest gap between consecutive sessions
4603
4603
  const sorted = recent.slice().sort((a, b) => {
4604
- const _aT = a.firstTs || a.mtime; const aTs = typeof _aT === 'number' ? _aT : Number(_aT) || new Date(_aT).getTime() || 0;
4605
- const _bT = b.firstTs || b.mtime; const bTs = typeof _bT === 'number' ? _bT : Number(_bT) || new Date(_bT).getTime() || 0;
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();
4606
4606
  return aTs - bTs;
4607
4607
  });
4608
4608
  let maxGapMs = 0; let gapStart = 0;
4609
4609
  for (let i = 1; i < sorted.length; i++) {
4610
4610
  const prev = sorted[i - 1];
4611
4611
  const curr = sorted[i];
4612
- 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();
4613
4613
  const prevEnd = prevTs + (prev.totalDurationMs || 60000);
4614
- 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();
4615
4615
  const gap = currTs - prevEnd;
4616
4616
  if (gap > maxGapMs) { maxGapMs = gap; gapStart = prevEnd; }
4617
4617
  }
@@ -4645,149 +4645,103 @@ function buildLoopSparkline(l) {
4645
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>`;
4646
4646
  }
4647
4647
 
4648
- function fmtInterval(v) {
4649
- if (!v && v !== 0) return '';
4650
- if (typeof v === 'string') return v;
4651
- const m = parseInt(v);
4652
- if (isNaN(m) || m <= 0) return String(v);
4653
- if (m < 60) return m + 'm';
4654
- if (m % 60 === 0) return (m / 60) + 'h';
4655
- return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
4656
- }
4657
-
4658
4648
  function fmtCountdown(nextAt) {
4659
- const ms = parseInt(nextAt) - Date.now();
4649
+ const ms = Number(nextAt) - Date.now();
4660
4650
  if (ms <= 0) return 'overdue';
4661
- const h = Math.floor(ms / 3600000);
4662
- const m = Math.floor((ms % 3600000) / 60000);
4663
- const s = Math.floor((ms % 60000) / 1000);
4664
- if (h > 0) return `next in ${h}h ${m}m`;
4665
- return m > 0 ? `next in ${m}m ${s}s` : `next in ${s}s`;
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`;
4666
4657
  }
4667
4658
 
4668
- function shortPath(p) {
4669
- if (!p) return '—';
4670
- const parts = p.replace(/\\/g, '/').split('/').filter(Boolean);
4671
- if (parts.length <= 2) return p;
4672
- return '…/' + parts.slice(-2).join('/');
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`;
4673
4666
  }
4674
4667
 
4675
4668
  // ── loops ──────────────────────────────────────────────────
4669
+ const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
4670
+
4676
4671
  async function renderLoops() {
4677
4672
  const el = document.getElementById('loops-content');
4678
4673
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
4679
4674
  try {
4680
4675
  const data = await apiFetch('/api/loops?dir=' + enc(DIR));
4681
- const loops = Array.isArray(data) ? data : (data.loops || []);
4682
- const hilCountL = loops.filter(l => l.status === 'hil:pending').length;
4683
- 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 || '—';
4684
4681
  if (!loops.length) {
4685
- el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops scheduled</div><div style="font-size:11px;color:var(--text-xs);margin-top:4px">Create one with <code>+ New Loop</code> above or via <code>npx monomind autodev --tillend</code></div></div>';
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>';
4686
4683
  return;
4687
4684
  }
4688
- // Deduplicate: hide hook/lock entries shadowed by _repeat.md entries
4689
- const hasRepeatLoops = loops.some(l => l.source === '_repeat.md');
4690
- const repeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
4691
- const dedupedLoops = loops.filter(l => {
4692
- if (l.source === 'scheduled_tasks_lock' && hasRepeatLoops) return false;
4693
- if (l.source !== 'schedule_wakeup_hook') return true;
4694
- const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
4695
- if (!m) return true;
4696
- return !repeatPrompts.has(m[1].trim());
4697
- });
4698
- el.innerHTML = dedupedLoops.map((l, idx) => {
4699
- const isHil = l.status === 'hil:pending';
4700
- const isTillend = l.type === 'tillend';
4701
- const curRep = l.currentRep || 0;
4702
- const maxReps = l.maxReps || 0;
4703
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
4704
- // Loops with status 'running'/'waiting'/'active' are explicitly active.
4705
- // Don't mark them overdue unless nextRunAt is >2h stale (loop died without cleanup).
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;
4706
4690
  const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
4707
- const LOOP_STALE_MS = 2 * 60 * 60 * 1000;
4708
- const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive &&
4709
- nextAt > 0 && nextAt <= Date.now();
4710
- const isStaledActive = isExplicitlyActive && nextAt > 0 &&
4711
- (Date.now() - nextAt) > LOOP_STALE_MS;
4712
- const isFinished = isOverdue || isStaledActive ||
4713
- (!isExplicitlyActive && maxReps > 0 && curRep >= maxReps) ||
4714
- l.status === 'finished' || l.status === 'done' ||
4715
- l.status === 'complete' || l.status === 'completed' || l.status === 'expired';
4716
- const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
4717
- const intervalStr = fmtInterval(l.interval || l.schedule);
4718
- // Parse loop into consistent: userPrompt / command / flags
4719
- const _lp = (function(_l) {
4720
- if (_l.command) {
4721
- const flags = [];
4722
- if (_l.type && _l.type !== 'repeat') flags.push('--' + _l.type);
4723
- if (_l.maxReps) flags.push('--maxruns ' + _l.maxReps);
4724
- if (_l.interval) { const s = String(_l.interval).match(/^(\d+)s$/); if (s) flags.push('--wait ' + s[1]); }
4725
- if (_l.currentRep != null) flags.push('--rep ' + _l.currentRep);
4726
- if (_l.id) flags.push('--loop ' + _l.id);
4727
- return { userPrompt: _l.prompt || '', command: _l.command, flagsStr: flags.join(' ') };
4728
- }
4729
- const full = _l.prompt || '';
4730
- const cmdM = full.match(/^(\/\S+)/);
4731
- if (!cmdM) return { userPrompt: full, command: '', flagsStr: '' };
4732
- const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
4733
- let ti = 0, fp = [];
4734
- while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
4735
- fp.push(tokens[ti++]);
4736
- if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) fp.push(tokens[ti++]);
4737
- }
4738
- return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1], flagsStr: fp.join(' ') };
4739
- })(l);
4740
- const userPrompt = _lp.userPrompt;
4741
- const cmdStr = _lp.command;
4742
- const flagsStr = _lp.flagsStr;
4743
- const fullPrompt = l.prompt || '';
4744
- const name = (l.name || userPrompt || cmdStr || 'loop').slice(0, 60);
4745
- const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
4746
- const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
4747
- const pct = (!isTillend && maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
4748
- const progBar = (!isTillend && maxReps > 0 && running)
4749
- ? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
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>`
4750
4713
  : '';
4751
- const runCountDisplay = isTillend
4752
- ? `run ${curRep} / ∞${maxReps > 0 ? ' (cap: ' + maxReps + ')' : ''}`
4753
- : (maxReps > 0 ? `${curRep} / ${maxReps}` : String(curRep || '—'));
4754
- const cdownSpan = nextAt
4755
- ? ` <span class="loop-cdown${nextAt - Date.now() <= 0 ? ' overdue' : ''}" data-nextat="${nextAt}">${fmtCountdown(nextAt)}</span>`
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>`
4756
4716
  : '';
4757
- const stopBtn = running
4758
- ? `<button class="loop-stop-btn" data-loop-id="${esc(l.id || l.name || String(idx))}" onclick="stopLoop(event, this.dataset.loopId)" title="Stop this loop">■ Stop</button>`
4759
- : '';
4760
- const typeBadge = `<span class="loop-type-badge${isTillend ? ' tillend' : ''}">${esc(l.type || 'repeat')}</span>`;
4761
- const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
4762
- const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
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>`;
4763
4720
  const hilBanner = isHil
4764
- ? `<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>`
4765
4722
  : '';
4766
- const metaParts = [intervalStr, l.description].filter(Boolean).join(' · ').slice(0, 80);
4767
- return `<div class="loop-row" data-loop-status="${esc(l.status || '')}" onclick="toggleLoop(this)">
4768
- <div class="loop-ico">${isTillend ? '' : ''}</div>
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>
4769
4727
  <div class="loop-body">
4770
- <div class="loop-name" title="${esc(userPrompt || fullPrompt)}">${typeBadge}${esc(name)}</div>
4771
- <div class="loop-meta">${esc(metaParts)}${cdownSpan}</div>
4772
- ${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>
4773
4730
  ${progBar}
4731
+ ${hilBanner}
4774
4732
  </div>
4775
- <div class="loop-status ${statusClass}">${statusLabel}</div>
4733
+ <div class="loop-status ${statusCls}">${statusLabel}${isHil ? ' ⚠' : ''}</div>
4776
4734
  ${stopBtn}
4777
4735
  </div>
4778
4736
  <div class="loop-expand">
4779
- ${userPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(userPrompt)}">${esc(userPrompt.slice(0, 300))}${userPrompt.length > 300 ? '…' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy prompt" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(userPrompt)}).then(()=>showToast('Copied','Prompt copied','ok'))">⎘</button></div></div>` : ''}
4780
- ${cmdStr ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="display:flex;align-items:center;gap:8px"><span>${esc(cmdStr)}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy command" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(cmdStr)}).then(()=>showToast('Copied','Command copied','ok'))">⎘</button></div></div>` : ''}
4781
- ${flagsStr ? `<div class="le-row"><div class="le-lbl">Flags</div><div class="le-val mono" style="display:flex;align-items:flex-start;gap:8px"><span title="${esc(flagsStr)}">${esc(flagsStr.slice(0, 300))}${flagsStr.length > 300 ? '' : ''}</span><button class="btn" style="flex-shrink:0;font-size:10px;padding:1px 6px" title="Copy flags" onclick="event.stopPropagation();navigator.clipboard.writeText(${JSON.stringify(flagsStr)}).then(()=>showToast('Copied','Flags copied','ok'))">⎘</button></div></div>` : ''}
4782
- <div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${esc(l.type || 'repeat')}</div></div>
4783
- <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(intervalStr || '')}</div></div>
4784
- <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isHil ? '⚠ hil:pending' : (running ? '● running' : (isFinished ? '✓ done' : '○ stopped'))}</div></div>
4785
- ${isHil && l.id ? `<div class="le-row"><div class="le-lbl">HIL file</div><div class="le-val mono" style="color:oklch(75% 0.16 60);word-break:break-all">.monomind/loops/${esc(l.id)}-hil.md</div></div>` : ''}
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>
4786
4742
  <div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
4787
- ${(()=>{ const sMs=l.startedAt?(typeof l.startedAt==='number'?l.startedAt:new Date(l.startedAt).getTime()):0; const age=sMs>0&&sMs<Date.now()?Date.now()-sMs:0; return age>0?`<div class="le-row"><div class="le-lbl">Running for</div><div class="le-val">${fmtDur(age)}</div></div>`:''; })()}
4788
- <div class="le-row"><div class="le-lbl">Last run</div><div class="le-val" title="${l.lastRunAt ? new Date(typeof l.lastRunAt === 'number' ? l.lastRunAt : Number(l.lastRunAt) || l.lastRunAt).toLocaleString() : ''}">${esc(lastRun)}</div></div>
4789
- <div class="le-row"><div class="le-lbl">${isTillend ? 'Progress' : 'Run count'}</div><div class="le-val">${esc(runCountDisplay)}</div></div>
4790
- ${l.source ? `<div class="le-row"><div class="le-lbl">Source</div><div class="le-val">${esc(l.source)}</div></div>` : ''}
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>
4791
4745
  ${buildLoopSparkline(l)}
4792
4746
  </div>`;
4793
4747
  }).join('');
@@ -4827,25 +4781,27 @@ async function stopLoop(evt, id) {
4827
4781
  const btn = evt.currentTarget;
4828
4782
  if (!btn.dataset.confirming) {
4829
4783
  btn.dataset.confirming = '1';
4784
+ const orig = btn.textContent;
4830
4785
  btn.textContent = '■ Confirm?';
4831
- btn.style.cssText = 'background:oklch(55% 0.2 25 / 0.25);color:oklch(72% 0.2 25);border-color:oklch(55% 0.2 25 / 0.4)';
4832
4786
  btn._resetTimer = setTimeout(() => {
4833
4787
  delete btn.dataset.confirming;
4834
- btn.textContent = '■ Stop';
4835
- btn.style.cssText = '';
4788
+ btn.textContent = orig;
4836
4789
  }, 3000);
4837
4790
  return;
4838
4791
  }
4839
4792
  clearTimeout(btn._resetTimer);
4840
4793
  delete btn.dataset.confirming;
4841
4794
  try {
4842
- await fetch('/api/loops/stop?dir=' + enc(DIR), {
4795
+ const r = await fetch('/api/loops/stop?dir=' + enc(DIR), {
4843
4796
  method: 'POST', headers: { 'Content-Type': 'application/json' },
4844
- body: JSON.stringify({ id })
4797
+ body: JSON.stringify({ id }),
4845
4798
  });
4846
- showToast('Stopped', 'Loop stopped', 'ok');
4847
- renderLoops();
4848
- } 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
+ }
4849
4805
  }
4850
4806
 
4851
4807
  let _cdownInterval = null;
@@ -4855,32 +4811,8 @@ function startCountdowns() {
4855
4811
  _cdownInterval = setInterval(() => {
4856
4812
  document.querySelectorAll('.loop-cdown[data-nextat]').forEach(el => {
4857
4813
  const ms = parseInt(el.dataset.nextat) - Date.now();
4858
- if (ms <= 0) {
4859
- const row = el.closest('.loop-row');
4860
- const loopStatus = row ? (row.dataset.loopStatus || '') : '';
4861
- const isActiveStatus = loopStatus === 'running' || loopStatus === 'waiting' || loopStatus === 'active';
4862
- if (isActiveStatus) {
4863
- // Loop is between rounds or executing — not done, just waiting for next update
4864
- el.textContent = 'executing…';
4865
- el.classList.remove('overdue');
4866
- } else {
4867
- el.textContent = 'overdue';
4868
- el.classList.add('overdue');
4869
- if (row) {
4870
- const badge = row.querySelector('.loop-status');
4871
- if (badge && badge.classList.contains('active')) {
4872
- badge.classList.remove('active');
4873
- badge.classList.add('stopped');
4874
- badge.textContent = 'done';
4875
- }
4876
- const stopBtn = row.querySelector('.loop-stop-btn');
4877
- if (stopBtn) stopBtn.remove();
4878
- }
4879
- }
4880
- return;
4881
- }
4882
4814
  el.textContent = fmtCountdown(el.dataset.nextat);
4883
- el.classList.remove('overdue');
4815
+ el.classList.toggle('overdue', ms <= 0);
4884
4816
  });
4885
4817
  }, 1000);
4886
4818
  }
@@ -4932,7 +4864,7 @@ function buildEfficiencyPanel() {
4932
4864
  <span class="eff-lbl" title="${esc(s.lastPrompt||s.id)}">${esc(lbl)}</span>
4933
4865
  <span class="eff-pct ${cls}">${pct}%</span>
4934
4866
  </div>
4935
- <div class="eff-bar-wrap" title="${esc(s.lastPrompt||s.id)}: ${pct}% cache efficiency"><div class="eff-bar-fill" style="width:${pct}%;background:${fillColor}"></div></div>`;
4867
+ <div class="eff-bar-wrap"><div class="eff-bar-fill" style="width:${pct}%;background:${fillColor}"></div></div>`;
4936
4868
  }).join('');
4937
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}`;
4938
4870
  }
@@ -4969,10 +4901,10 @@ function renderModelMix() {
4969
4901
  const short = model.replace(/^claude-/,'').replace(/-\d{8}$/,'');
4970
4902
  const pct = totalCost > 0 ? Math.round(d.cost/totalCost*100) : 0;
4971
4903
  return `<tr>
4972
- <td style="font-size:11px" title="${esc(model)}">${esc(short)}</td>
4904
+ <td style="font-size:11px">${esc(short)}</td>
4973
4905
  <td class="lb-cost">$${d.cost.toFixed(2)}</td>
4974
4906
  <td class="lb-dur">${pct}%</td>
4975
- <td class="lb-dur">${d.calls.toLocaleString()}</td>
4907
+ <td class="lb-dur">${d.calls}</td>
4976
4908
  </tr>`;
4977
4909
  }).join('')}
4978
4910
  </tbody></table>`;
@@ -4991,14 +4923,14 @@ function buildWeeklyRecap() {
4991
4923
  const weekStart = new Date(); weekStart.setDate(weekStart.getDate() - weekStart.getDay()); weekStart.setHours(0,0,0,0);
4992
4924
  const weekSess = allSessions.filter(s => {
4993
4925
  const t = s.lastTs || s.mtime;
4994
- 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();
4995
4927
  });
4996
4928
  if (!weekSess.length) return;
4997
4929
  const totalCost = weekSess.reduce((a,s) => a + (s.totalCost||0), 0);
4998
4930
  const totalTools = weekSess.reduce((a,s) => a + (s.toolCalls||0), 0);
4999
4931
  const days = new Set(weekSess.map(s => {
5000
4932
  const t = s.lastTs || s.mtime;
5001
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString();
4933
+ return new Date(typeof t === 'number' ? t : t).toDateString();
5002
4934
  })).size;
5003
4935
  const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
5004
4936
  const streak = calcStreak();
@@ -5066,12 +4998,11 @@ function buildActivityHeatmap() {
5066
4998
  for (const s of allSessions) {
5067
4999
  const t = s.firstTs || s.mtime;
5068
5000
  if (!t) continue;
5069
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
5001
+ const d = new Date(typeof t === 'number' ? t : t);
5070
5002
  grid[d.getDay()][d.getHours()]++;
5071
5003
  }
5072
5004
  const maxVal = Math.max(1, ...grid.flat());
5073
5005
  const DAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];
5074
- const FULL_DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
5075
5006
  let html = '<div class="heatmap-grid">';
5076
5007
  html += '<div class="heatmap-row"><div class="heatmap-lbl"></div>';
5077
5008
  for (let h = 0; h < 24; h++) {
@@ -5084,7 +5015,7 @@ function buildActivityHeatmap() {
5084
5015
  const v = grid[d][h];
5085
5016
  const alpha = v > 0 ? Math.max(0.18, v/maxVal).toFixed(2) : 0;
5086
5017
  const bg = v > 0 ? `oklch(65% 0.18 200 / ${alpha})` : 'transparent';
5087
- html += `<div class="heatmap-cell" style="background:${bg};border:1px solid ${v>0?'transparent':'var(--border)'}" title="${FULL_DAYS[d]} ${h}:00–${h+1}:00 — ${v} session${v!==1?'s':''}"></div>`;
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>`;
5088
5019
  }
5089
5020
  html += '</div>';
5090
5021
  }
@@ -5166,8 +5097,8 @@ function v2RenderOrgList() {
5166
5097
  return `<div class="org-item${active}" data-org="${esc(o.name)}" onclick="v2SelectOrg(this.dataset.org)">
5167
5098
  <div class="oi-dot ${o.running ? 'running' : ''}"></div>
5168
5099
  <div class="oi-body">
5169
- <div class="oi-name" title="${esc(o.name)}">${esc(o.name)}</div>
5170
- ${goalSnip ? `<div class="oi-goal" title="${esc(o.goal || goalSnip)}">${esc(goalSnip)}</div>` : ''}
5100
+ <div class="oi-name">${esc(o.name)}</div>
5101
+ ${goalSnip ? `<div class="oi-goal">${esc(goalSnip)}</div>` : ''}
5171
5102
  <div class="oi-chips">
5172
5103
  ${o.running ? '<span class="oi-chip live">LIVE</span>' : ''}
5173
5104
  <span class="oi-chip">${rolesN} roles</span>
@@ -5633,7 +5564,7 @@ function v2RenderOrgRoles() {
5633
5564
  const pane = document.getElementById('odt-roles');
5634
5565
  if (!pane) return;
5635
5566
  if (!roles.length) {
5636
- pane.innerHTML = '<div class="empty">No roles defined<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Roles are defined in the org config. Create an org with <code>npx monomind swarm init</code></div></div>';
5567
+ pane.innerHTML = '<div class="empty">No roles defined</div>';
5637
5568
  return;
5638
5569
  }
5639
5570
  // Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
@@ -5705,7 +5636,7 @@ function v2RenderAgentDrawer(data) {
5705
5636
  headEl.innerHTML = `
5706
5637
  <img class="oad-avatar" src="${avatar}" alt="${esc(name)}" onerror="this.src='data/avatars/coder.svg'"/>
5707
5638
  <div class="oad-id">
5708
- <div class="oad-name" title="${esc(name)}">${esc(name)}</div>
5639
+ <div class="oad-name">${esc(name)}</div>
5709
5640
  <div class="oad-pills">
5710
5641
  ${type ? `<span class="oad-pill">${esc(type)}</span>` : ''}
5711
5642
  ${model ? `<span class="oad-pill model">${esc(model)}</span>` : ''}
@@ -5783,7 +5714,7 @@ function v2RenderOrgActivity() {
5783
5714
  if (!_v2SelOrg) return;
5784
5715
  const activity = _v2OrgData?._activity || [];
5785
5716
  const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
5786
- 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);
5787
5718
  const pane = document.getElementById('odt-activity');
5788
5719
  if (!pane) return;
5789
5720
  const fmtOrgEvType = t => {
@@ -5791,14 +5722,13 @@ function v2RenderOrgActivity() {
5791
5722
  return m[t]||(t||'').replace(/^org:/,'');
5792
5723
  };
5793
5724
  if (!events.length) {
5794
- pane.innerHTML = '<div class="empty">No activity recorded<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Activity is emitted when org agents run. Start an org with <code>npx monomind swarm start</code></div></div>';
5725
+ pane.innerHTML = '<div class="empty">No activity recorded</div>';
5795
5726
  return;
5796
5727
  }
5797
5728
  pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
5798
- const t = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
5729
+ const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
5799
5730
  const detail = ev.role||ev.msg||ev.agent||'';
5800
- const tFull = ev.ts ? new Date(typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || ev.ts).toLocaleString() : '';
5801
- return `<div class="av2-row"><span class="av2-time" title="${esc(tFull)}">${esc(t)}</span><span class="av2-type">${esc(fmtOrgEvType(ev.type))}</span><span class="av2-msg">${esc(detail)}</span></div>`;
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>`;
5802
5732
  }).join('')}</div>`;
5803
5733
  }
5804
5734
 
@@ -5836,12 +5766,12 @@ function v2RenderOrgHeartbeats() {
5836
5766
  status: a.status || 'idle',
5837
5767
  }));
5838
5768
  }
5839
- if (!hb.length) { pane.innerHTML = '<div class="empty">No agents to report<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Agent heartbeats appear when agents are running. Spawn one with <code>npx monomind agent spawn</code></div></div>'; return; }
5769
+ if (!hb.length) { pane.innerHTML = '<div class="empty">No agents to report</div>'; return; }
5840
5770
  const cls = (st) => (st === 'active' || st === 'running') ? 'on' : ((st === 'error' || st === 'failed') ? 'warn' : '');
5841
5771
  pane.innerHTML = `<div class="m-group-title">Agent Heartbeats</div>` +
5842
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">
5843
- <span style="color:var(--text-hi);font-family:var(--mono);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(h.agent || '')}">${esc(h.agent || '—')}</span>
5844
- <span style="color:var(--text-lo);white-space:nowrap" title="${h.ts ? new Date(typeof h.ts === 'number' ? h.ts : Number(h.ts) || h.ts).toLocaleString() : ''}">${h.ts ? relTime(h.ts) : 'never'}</span>
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>
5845
5775
  <span class="ss-pill ${cls(h.status)}">${esc(String(h.status || 'idle').toUpperCase())}</span>
5846
5776
  </div>`).join('');
5847
5777
  }
@@ -5860,15 +5790,15 @@ function v2RenderOrgTasks() {
5860
5790
  (Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: t.status || col }));
5861
5791
  }
5862
5792
  }
5863
- if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Tasks are created automatically when agents run. Start one with <code>npx monomind agent spawn</code></div></div>'; return; }
5793
+ if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks</div>'; return; }
5864
5794
  const rank = { running: 0, doing: 0, todo: 1, pending: 1, done: 2 };
5865
5795
  tasks.sort((a, b) => (rank[a.status] ?? 1) - (rank[b.status] ?? 1));
5866
5796
  const pill = (st) => st === 'done' ? 'on' : (st === 'running' || st === 'doing' ? 'warn' : '');
5867
5797
  pane.innerHTML = `<div class="m-group-title">Tasks (${tasks.length})</div>` +
5868
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">
5869
5799
  <span class="ss-pill ${pill(t.status)}">${esc(t.status || '?')}</span>
5870
- <span style="flex:1;color:var(--text-hi);font-family:var(--mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.description || t.title || t.id || '')}">${esc(t.description || t.title || t.id || '—')}</span>
5871
- <span style="color:var(--text-lo);white-space:nowrap" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(t.ts || t.created_at || t.updated_at)}">${relTime(t.ts || t.created_at || t.updated_at)}</span>
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>
5872
5802
  </div>`).join('');
5873
5803
  }
5874
5804
 
@@ -5892,7 +5822,7 @@ function v2RenderOrgCosts() {
5892
5822
  ? c.map(r => ({ label: r.label ?? r.name ?? '—', cost: Number(r.value ?? r.cost ?? 0), tin: 0, tout: 0 }))
5893
5823
  : Object.entries(c).map(([k, v]) => ({ label: k, cost: Number(v) || 0, tin: 0, tout: 0 }));
5894
5824
  }
5895
- if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Cost tracking starts when agents run. Set a budget via the Org Settings tab.</div></div>'; return; }
5825
+ if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data</div>'; return; }
5896
5826
  const cur = (b && b.currency) || 'USD';
5897
5827
  const period = (b && b.period) || '';
5898
5828
  const total = rows.reduce((s, r) => s + r.cost, 0);
@@ -5904,7 +5834,7 @@ function v2RenderOrgCosts() {
5904
5834
  <span style="color:var(--accent);font-family:var(--mono);font-size:15px;font-weight:600">$${total.toFixed(4)}</span>
5905
5835
  </div>` +
5906
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">
5907
- <span style="color:var(--text-hi);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(String(r.label))}">${esc(String(r.label))}</span>
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>
5908
5838
  <span style="color:var(--text-lo);font-family:var(--mono);font-size:10px;white-space:nowrap">${(r.tin + r.tout).toLocaleString()} tok</span>
5909
5839
  <span style="color:var(--accent);font-family:var(--mono);white-space:nowrap">$${r.cost.toFixed(4)}</span>
5910
5840
  </div>`).join('') +
@@ -5921,14 +5851,14 @@ function v2RenderOrgMembers() {
5921
5851
  const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
5922
5852
  const list = members.length ? members : roles;
5923
5853
  const joinReqs = Array.isArray(_v2OrgData?._joinRequests) ? _v2OrgData._joinRequests : [];
5924
- if (!list.length) { pane.innerHTML = '<div class="empty">No members<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Add agents or humans via <code>npx monomind agent spawn --org &lt;name&gt;</code></div></div>'; return; }
5854
+ if (!list.length) { pane.innerHTML = '<div class="empty">No members</div>'; return; }
5925
5855
  const src = members.length ? 'joined members' : 'defined roles';
5926
5856
  const active = (r) => r.running || r.active || r.status === 'active';
5927
5857
  pane.innerHTML = `<div class="m-group-title">Members (${list.length}) · ${src}</div>` +
5928
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">
5929
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>
5930
5860
  <div style="flex:1;min-width:0">
5931
- <div style="color:var(--text-hi);font-family:var(--mono);font-size:12px" title="${esc(r.name || r.id || r.user || '')}">${esc(r.name || r.id || r.user || '—')}</div>
5861
+ <div style="color:var(--text-hi);font-family:var(--mono);font-size:12px">${esc(r.name || r.id || r.user || '—')}</div>
5932
5862
  <div style="color:var(--text-lo);font-size:10px">${esc(r.title || r.type || r.role || '')}</div>
5933
5863
  </div>
5934
5864
  <span class="ss-pill ${active(r) ? 'on' : ''}">${active(r) ? 'ACTIVE' : 'IDLE'}</span>
@@ -5942,7 +5872,7 @@ function v2RenderOrgGoals() {
5942
5872
  const el = document.getElementById('odt-goals');
5943
5873
  if (!el || !_v2OrgData) return;
5944
5874
  const goals = _v2OrgData.goals || _v2OrgData.config?.goals || [];
5945
- if (!goals.length) { el.innerHTML = '<div class="empty">No goals defined<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Add goals to the org config via <code>npx monomind swarm config --goal "…"</code></div></div>'; return; }
5875
+ if (!goals.length) { el.innerHTML = '<div class="empty">No goals defined</div>'; return; }
5946
5876
  function renderGoal(g, depth) {
5947
5877
  if (depth > 20) return ''; // Depth guard to prevent stack overflow
5948
5878
  const indent = depth * 20;
@@ -5966,7 +5896,7 @@ function v2RenderOrgBoard() {
5966
5896
  const el = document.getElementById('odt-board');
5967
5897
  if (!el || !_v2OrgData) return;
5968
5898
  const issues = _v2OrgData._issues || [];
5969
- if (!issues.length) { el.innerHTML = '<div class="empty">No issues<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Issues are created by agents during task execution or via the Issues tab.</div></div>'; return; }
5899
+ if (!issues.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
5970
5900
  const cols = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
5971
5901
  const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
5972
5902
  el.innerHTML = `<div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">` +
@@ -5975,10 +5905,9 @@ function v2RenderOrgBoard() {
5975
5905
  return `<div style="min-width:160px;flex:1">
5976
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>
5977
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">
5978
- <div style="color:var(--text-hi)" title="${esc(i.title || i.description || '')}">${PRIORITY[i.priority] || ''} ${esc((i.title || i.description || '—').slice(0, 60))}${(i.title || i.description || '').length > 60 ? '…' : ''}</div>
5908
+ <div style="color:var(--text-hi)">${PRIORITY[i.priority] || ''} ${esc((i.title || i.description || '—').slice(0, 60))}</div>
5979
5909
  ${i.assignee ? `<div style="font-size:10px;color:var(--text-lo);margin-top:3px">${esc(i.assignee)}</div>` : ''}
5980
5910
  </div>`).join('')}
5981
- ${cards.length > 15 ? `<div style="font-size:11px;color:var(--text-xs);text-align:center;padding:4px 0">+${cards.length - 15} more</div>` : ''}
5982
5911
  </div>`;
5983
5912
  }).join('') + `</div>`;
5984
5913
  }
@@ -6002,9 +5931,9 @@ function v2RenderOrgLive() {
6002
5931
  <div class="m-group-title" style="margin-bottom:6px">Activity Feed</div>
6003
5932
  <div style="max-height:240px;overflow-y:auto;font-size:11px;font-family:var(--mono)">
6004
5933
  ${(_v2OrgData._activity || []).slice(-30).reverse().map(e => `<div style="padding:3px 0;border-bottom:1px solid var(--border);color:var(--text-lo)">
6005
- <span title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(e.ts || e.timestamp || e.created_at)}">${esc(relTime(e.ts || e.timestamp || e.created_at))}</span>
5934
+ ${esc(relTime(e.ts || e.timestamp || e.created_at))}
6006
5935
  <span style="color:var(--text-mid);margin-left:6px">${esc(e.type || e.kind || e.event || '—')}</span>
6007
- ${e.message ? `<span style="color:var(--text-hi);margin-left:6px" title="${esc(e.message.toString())}">${esc(e.message.toString().slice(0, 80))}${e.message.toString().length > 80 ? '…' : ''}</span>` : ''}
5936
+ ${e.message ? `<span style="color:var(--text-hi);margin-left:6px">${esc(e.message.toString().slice(0, 80))}</span>` : ''}
6008
5937
  </div>`).join('')}
6009
5938
  </div>`;
6010
5939
  // auto-refresh every 5s while tab is active
@@ -6029,7 +5958,7 @@ async function v2RenderOrgApprovals() {
6029
5958
  const _enc = encodeURIComponent(_v2SelOrg);
6030
5959
  const data = await fetch(`/api/org/${_enc}/approvals${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6031
5960
  const approvals = Array.isArray(data) ? data : (data.approvals || []);
6032
- if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Approvals appear when agents request human confirmation before taking a sensitive action.</div></div>'; return; }
5961
+ if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals</div>'; return; }
6033
5962
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6034
5963
  <thead><tr style="color:var(--text-xs);text-align:left">
6035
5964
  <th style="padding:6px 8px">Requester</th><th>Action</th><th>Status</th><th>Date</th><th></th>
@@ -6040,13 +5969,13 @@ async function v2RenderOrgApprovals() {
6040
5969
  const aid = a.id || '';
6041
5970
  return `<tr style="border-top:1px solid var(--border)">
6042
5971
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(a.requester || a.agent || '—')}</td>
6043
- <td style="padding:6px 8px;color:var(--text-lo);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(a.action || a.description || '')}">${esc((a.action || a.description || '').slice(0, 80))}${(a.action || a.description || '').length > 80 ? '…' : ''}</td>
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>
6044
5973
  <td style="padding:6px 8px"><span class="ss-pill ${cls}">${esc(a.status || 'pending')}</span></td>
6045
- <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(a.created_at || a.ts)}">${relTime(a.created_at || a.ts)}</td>
5974
+ <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
6046
5975
  <td style="padding:6px 8px;white-space:nowrap">
6047
5976
  ${pending
6048
- ? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" onclick="orgApprovalAction(${JSON.stringify(aid).replace(/"/g, '&quot;')},'approve')">✓</button>
6049
- <button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" onclick="orgApprovalAction(${JSON.stringify(aid).replace(/"/g, '&quot;')},'reject')">✕</button>`
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>`
6050
5979
  : ''}
6051
5980
  </td>
6052
5981
  </tr>`;
@@ -6082,7 +6011,7 @@ async function v2RenderOrgSecrets() {
6082
6011
  ${s.purpose ? `<span style="color:var(--text-lo)">${esc(s.purpose)}</span>` : ''}
6083
6012
  <span style="margin-left:auto;font-family:var(--mono);color:var(--border)">••••••••</span>
6084
6013
  </div>`).join('')
6085
- : '<div class="empty">No secrets configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Add secrets to the org config under <code>secrets: []</code> — values are never stored in the dashboard.</div></div>');
6014
+ : '<div class="empty">No secrets configured</div>');
6086
6015
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
6087
6016
  }
6088
6017
 
@@ -6099,19 +6028,19 @@ function v2RenderOrgSettings() {
6099
6028
  el.innerHTML = `
6100
6029
  <div style="font-size:11px;color:var(--text-lo);margin-bottom:14px">Changes generate a CLI command — no direct writes from UI.</div>
6101
6030
  <div style="display:flex;flex-direction:column;gap:12px;max-width:380px">
6102
- <div><div class="le-lbl">Goal</div><input id="os-goal" class="filter-input" placeholder="Describe the org's mission…" value="${esc(d.goal || '')}"></div>
6031
+ <div><div class="le-lbl">Goal</div><input id="os-goal" class="filter-input" value="${esc(d.goal || '')}"></div>
6103
6032
  <div><div class="le-lbl">Topology</div>
6104
- <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">
6105
6034
  ${topos.map(t => `<option ${d.topology === t ? 'selected' : ''}>${esc(t)}</option>`).join('')}
6106
6035
  </select>
6107
6036
  </div>
6108
6037
  <div><div class="le-lbl">Governance</div>
6109
- <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">
6110
6039
  ${govs.map(g => `<option ${govVal === g ? 'selected' : ''}>${esc(g)}</option>`).join('')}
6111
6040
  </select>
6112
6041
  </div>
6113
- <div><div class="le-lbl">Budget (tokens)</div><input id="os-budget" class="filter-input" type="number" placeholder="e.g. 100000" value="${esc(String(budgetVal || ''))}"></div>
6114
- <button class="btn" title="Generate a monomind CLI command from these settings" style="width:fit-content;color:var(--accent);border-color:var(--accent)" onclick="generateOrgSettingsCmd()">Generate CLI Command</button>
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>
6115
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>
6116
6045
  </div>`;
6117
6046
  }
@@ -6141,7 +6070,7 @@ async function v2RenderOrgRoutines() {
6141
6070
  const _enc = encodeURIComponent(_v2SelOrg);
6142
6071
  const data = await fetch(`/api/org/${_enc}/routines${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6143
6072
  const routines = Array.isArray(data) ? data : (data.routines || _v2OrgData?.config?.routines || []);
6144
- if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Routines are scheduled agent tasks. Add them to the org config under <code>routines: []</code></div></div>'; return; }
6073
+ if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured</div>'; return; }
6145
6074
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6146
6075
  <thead><tr style="color:var(--text-xs);text-align:left">
6147
6076
  <th style="padding:5px 8px">Name</th><th>Cron</th><th>Last Run</th><th>Status</th>
@@ -6149,7 +6078,7 @@ async function v2RenderOrgRoutines() {
6149
6078
  routines.map(r => `<tr style="border-top:1px solid var(--border)">
6150
6079
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(r.name || '—')}</td>
6151
6080
  <td style="padding:6px 8px;font-family:var(--mono);color:var(--text-lo)">${esc(r.cron || r.schedule || '—')}</td>
6152
- <td style="padding:6px 8px;color:var(--text-lo)" title="${r.lastRun ? new Date(typeof r.lastRun === 'number' ? r.lastRun : Number(r.lastRun) || r.lastRun).toLocaleString() : ''}">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
6081
+ <td style="padding:6px 8px;color:var(--text-lo)">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
6153
6082
  <td style="padding:6px 8px"><span class="ss-pill ${r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || '—')}</span></td>
6154
6083
  </tr>`).join('') + '</tbody></table>';
6155
6084
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6164,7 +6093,7 @@ async function v2RenderOrgMyIssues() {
6164
6093
  const _enc = encodeURIComponent(_v2SelOrg);
6165
6094
  const data = await fetch(`/api/org/${_enc}/my-issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6166
6095
  const issues = Array.isArray(data) ? data : (data.issues || []);
6167
- if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Issues are assigned by agents or via the Board tab. Check the Issues tab to see all open issues.</div></div>'; return; }
6096
+ if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you</div>'; return; }
6168
6097
  const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
6169
6098
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6170
6099
  <thead><tr style="color:var(--text-xs);text-align:left">
@@ -6174,9 +6103,9 @@ async function v2RenderOrgMyIssues() {
6174
6103
  const cls = i.status === 'done' ? 'on' : i.status === 'blocked' ? 'warn' : '';
6175
6104
  return `<tr style="border-top:1px solid var(--border)">
6176
6105
  <td style="padding:5px 4px">${PRIORITY[i.priority] || '·'}</td>
6177
- <td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(i.title || i.description || '')}">${esc((i.title || i.description || '—').slice(0, 80))}${(i.title || i.description || '').length > 80 ? '…' : ''}</td>
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>
6178
6107
  <td style="padding:5px 8px"><span class="ss-pill ${cls}">${esc(i.status || 'open')}</span></td>
6179
- <td style="padding:5px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(i.updated_at || i.ts)}">${relTime(i.updated_at || i.ts)}</td>
6108
+ <td style="padding:5px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(i.updated_at || i.ts)}</td>
6180
6109
  </tr>`;
6181
6110
  }).join('') + '</tbody></table>';
6182
6111
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6205,21 +6134,19 @@ function v2RenderOrgBudgets() {
6205
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>`;
6206
6135
  }
6207
6136
  if (agents.length) {
6208
- const shownAgents = agents.slice(0, 20);
6209
6137
  html += '<div class="m-group-title" style="margin-bottom:6px">Per Agent</div>' +
6210
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>` +
6211
- shownAgents.map(a => {
6139
+ agents.slice(0, 20).map(a => {
6212
6140
  const over = a.budgetLimit && ((a.tokensIn || 0) + (a.tokensOut || 0)) > a.budgetLimit;
6213
6141
  return `<tr style="border-top:1px solid var(--border)${over ? ';color:var(--red)' : ''}">
6214
- <td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}" title="${esc((a.id || a.title || '').toString())}">${esc((a.id || a.title || '—').toString().slice(0, 14))}${(a.id || a.title || '').toString().length > 14 ? '…' : ''}</td>
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>
6215
6143
  <td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensIn || 0).toLocaleString()}</td>
6216
6144
  <td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensOut || 0).toLocaleString()}</td>
6217
6145
  <td style="padding:4px 8px;color:var(--accent)">$${Number(a.cost || 0).toFixed(4)}${over ? ' ⚠' : ''}</td>
6218
6146
  </tr>`;
6219
- }).join('') + '</tbody></table>' +
6220
- (agents.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${agents.length} agents</div>` : '');
6147
+ }).join('') + '</tbody></table>';
6221
6148
  }
6222
- el.innerHTML = html || '<div class="empty">No budget data<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Set token budgets in Org Settings to track spend per agent.</div></div>';
6149
+ el.innerHTML = html || '<div class="empty">No budget data</div>';
6223
6150
  }
6224
6151
 
6225
6152
  // ── PLUGINS ────────────────────────────────────────────────
@@ -6231,14 +6158,14 @@ async function v2RenderOrgPlugins() {
6231
6158
  const _enc = encodeURIComponent(_v2SelOrg);
6232
6159
  const data = await fetch(`/api/org/${_enc}/plugins${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6233
6160
  const plugins = Array.isArray(data) ? data : (data.plugins || []);
6234
- if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Browse and install plugins with <code>npx monomind plugins list</code></div></div>'; return; }
6161
+ if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
6235
6162
  el.innerHTML = `<div class="proj-grid">` +
6236
6163
  plugins.map(p => {
6237
6164
  const status = p.status || 'installed';
6238
6165
  const col = status === 'error' ? 'var(--red)' : status === 'installed' ? 'var(--accent)' : 'var(--text-lo)';
6239
6166
  return `<div class="proj-card">
6240
- <div class="proj-card-name" title="${esc(p.name || '')}">${esc(p.name || '—')}</div>
6241
- <div class="proj-card-path" title="${esc(p.description || '')}">${esc((p.description || '').slice(0, 80))}${(p.description || '').length > 80 ? '…' : ''}</div>
6167
+ <div class="proj-card-name">${esc(p.name || '—')}</div>
6168
+ <div class="proj-card-path">${esc((p.description || '').slice(0, 80))}</div>
6242
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>
6243
6170
  </div>`;
6244
6171
  }).join('') + `</div>`;
@@ -6261,7 +6188,7 @@ function v2RenderOrgCharts() {
6261
6188
  events: 0, errors: 0,
6262
6189
  }));
6263
6190
  activity.forEach(e => {
6264
- 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();
6265
6192
  const idx = buckets.findIndex((b, i) =>
6266
6193
  ts >= b.ts && (i === 13 || ts < buckets[i + 1].ts));
6267
6194
  if (idx >= 0) {
@@ -6286,7 +6213,7 @@ function v2RenderOrgCharts() {
6286
6213
  // Per-agent run bars
6287
6214
  const agentRuns = agents.slice(0, 10).map(a => {
6288
6215
  const runs = activity.filter(e => e.agentId === a.id || e.agent === a.id).length;
6289
- return { name: (a.type || a.title || a.id || '—').toString(), runs };
6216
+ return { name: (a.type || a.title || a.id || '—').toString().slice(0, 20), runs };
6290
6217
  }).filter(a => a.runs > 0).sort((x, y) => y.runs - x.runs);
6291
6218
  const maxRuns = Math.max(...agentRuns.map(a => a.runs), 1);
6292
6219
 
@@ -6306,7 +6233,7 @@ function v2RenderOrgCharts() {
6306
6233
  <div style="display:flex;gap:2px;align-items:flex-end;padding-bottom:28px;margin-bottom:16px;overflow-x:auto">${heatmap}</div>
6307
6234
  ${agentRuns.length ? `<div class="m-group-title" style="margin-bottom:6px">Per-Agent Runs</div>
6308
6235
  ${agentRuns.map(a => `<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">
6309
- <div style="width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc(a.name)}">${esc(a.name.slice(0, 20))}${a.name.length > 20 ? '…' : ''}</div>
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>
6310
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>
6311
6238
  <div style="width:30px;text-align:right;color:var(--text-lo);font-family:var(--mono);font-size:11px">${a.runs}</div>
6312
6239
  </div>`).join('')}` : ''}
@@ -6322,10 +6249,10 @@ async function v2RenderOrgProjects() {
6322
6249
  const _enc = encodeURIComponent(_v2SelOrg);
6323
6250
  const data = await fetch(`/api/org/${_enc}/projects${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6324
6251
  const projects = Array.isArray(data) ? data : (data.projects || []);
6325
- if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Run <code>npx monomind init</code> inside a project to add it.</div></div>'; return; }
6252
+ if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured</div>'; return; }
6326
6253
  el.innerHTML = `<div class="proj-grid">${projects.map(p => `<div class="proj-card">
6327
- <div class="proj-card-name" title="${esc(p.name || p.title || '')}">${esc(p.name || p.title || '—')}</div>
6328
- <div class="proj-card-path" title="${esc(p.description || p.path || '')}">${esc((p.description || p.path || '').slice(0,80))}${(p.description || p.path || '').length > 80 ? '…' : ''}</div>
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>
6329
6256
  <div class="proj-card-stats" style="margin-top:8px">
6330
6257
  ${p.status ? `<span class="ss-pill ${p.status==='active'?'on':''}">${esc(p.status)}</span>` : ''}
6331
6258
  </div>
@@ -6338,7 +6265,7 @@ async function v2RenderOrgSkills() {
6338
6265
  const el = document.getElementById('odt-skills');
6339
6266
  if (!el || !_v2OrgData) return;
6340
6267
  const roles = Array.isArray(_v2OrgData.roles) ? _v2OrgData.roles : [];
6341
- if (!roles.length) { el.innerHTML = '<div class="empty">No roles defined<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Skills come from agent roles. Create an org with <code>npx monomind swarm init</code></div></div>'; return; }
6268
+ if (!roles.length) { el.innerHTML = '<div class="empty">No roles defined</div>'; return; }
6342
6269
  el.innerHTML = '<div class="empty">Loading skills…</div>';
6343
6270
  const org = _v2SelOrg, dirq = DIR ? '?dir=' + encodeURIComponent(DIR) : '';
6344
6271
  // Enrich each role with expertise + task types from its agent definition (same source as the detail drawer)
@@ -6377,7 +6304,7 @@ async function v2RenderOrgWorkspaces() {
6377
6304
  const _enc = encodeURIComponent(_v2SelOrg);
6378
6305
  const data = await fetch(`/api/org/${_enc}/workspaces${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6379
6306
  const ws = Array.isArray(data) ? data : (data.workspaces || []);
6380
- if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Workspaces are project directories added to this org. Add one via Org Settings.</div></div>'; return; }
6307
+ if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured</div>'; return; }
6381
6308
  el.innerHTML = ws.map(w => `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
6382
6309
  <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
6383
6310
  <span style="font-size:13px;color:var(--text-hi);font-weight:500">${esc(w.name || w.id || '—')}</span>
@@ -6398,16 +6325,16 @@ async function v2RenderOrgInvites() {
6398
6325
  const _enc = encodeURIComponent(_v2SelOrg);
6399
6326
  const data = await fetch(`/api/org/${_enc}/invites${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6400
6327
  const invites = Array.isArray(data) ? data : (data.invites || []);
6401
- if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Generate an invite token to add members to this org.</div></div>'; return; }
6328
+ if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens</div>'; return; }
6402
6329
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6403
6330
  <thead><tr style="color:var(--text-xs);text-align:left">
6404
6331
  <th style="padding:5px 8px">Token</th><th>Role</th><th>Expires</th><th>Created</th>
6405
6332
  </tr></thead>
6406
6333
  <tbody>${invites.map(i => `<tr style="border-top:1px solid var(--border)">
6407
- <td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc(i.token||i.id||'')}">${esc((i.token||i.id||'—').slice(0,16))}…</td>
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>
6408
6335
  <td style="padding:5px 8px"><span class="ss-pill">${esc(i.role||'viewer')}</span></td>
6409
- <td style="padding:5px 8px;color:var(--text-lo)" title="${i.expiresAt ? new Date(typeof i.expiresAt === 'number' ? i.expiresAt : Number(i.expiresAt) || i.expiresAt).toLocaleString() : ''}">${i.expiresAt ? relTime(i.expiresAt) : '—'}</td>
6410
- <td style="padding:5px 8px;color:var(--text-lo)" title="${i.createdAt ? new Date(typeof i.createdAt === 'number' ? i.createdAt : Number(i.createdAt) || i.createdAt).toLocaleString() : ''}">${i.createdAt ? relTime(i.createdAt) : '—'}</td>
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>
6411
6338
  </tr>`).join('')}</tbody>
6412
6339
  </table>`;
6413
6340
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6418,17 +6345,17 @@ function v2RenderOrgAgentsFull() {
6418
6345
  const el = document.getElementById('odt-agents-full');
6419
6346
  if (!el || !_v2OrgData) return;
6420
6347
  const agents = _v2OrgData._agents || [];
6421
- if (!agents.length) { el.innerHTML = '<div class="empty">No agents found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Agents join this org when spawned with <code>npx monomind agent spawn --org &lt;name&gt;</code></div></div>'; return; }
6348
+ if (!agents.length) { el.innerHTML = '<div class="empty">No agents found</div>'; return; }
6422
6349
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6423
6350
  <thead><tr style="color:var(--text-xs);text-align:left">
6424
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>
6425
6352
  </tr></thead>
6426
6353
  <tbody>${agents.map(a => `<tr style="border-top:1px solid var(--border)">
6427
- <td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="${esc((a.id||'').toString())}">${esc((a.id||'—').toString().slice(0,12))}${(a.id||'').toString().length > 12 ? '…' : ''}</td>
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>
6428
6355
  <td style="padding:5px 8px;color:var(--text-hi)">${esc(a.type||a.title||'—')}</td>
6429
6356
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapter||'—')}</td>
6430
6357
  <td style="padding:5px 8px"><span class="ss-pill ${(a.status==='running'||a.running)?'on':''}">${esc(a.status||'idle')}</span></td>
6431
- <td style="padding:5px 8px;color:var(--text-lo)" title="${a.lastHeartbeat ? new Date(typeof a.lastHeartbeat === 'number' ? a.lastHeartbeat : Number(a.lastHeartbeat) || a.lastHeartbeat).toLocaleString() : ''}">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
6358
+ <td style="padding:5px 8px;color:var(--text-lo)">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
6432
6359
  <td style="padding:5px 8px;color:var(--text-lo)">${a.tokensIn != null ? Number(a.tokensIn).toLocaleString() : '—'}</td>
6433
6360
  <td style="padding:5px 8px;color:var(--text-lo)">${a.tokensOut != null ? Number(a.tokensOut).toLocaleString() : '—'}</td>
6434
6361
  </tr>`).join('')}</tbody>
@@ -6444,7 +6371,7 @@ async function v2RenderOrgEnvironments() {
6444
6371
  const _enc = encodeURIComponent(_v2SelOrg);
6445
6372
  const data = await fetch(`/api/org/${_enc}/environments${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6446
6373
  const envs = Array.isArray(data) ? data : (data.environments || []);
6447
- if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Environments (dev/staging/prod) are declared in the org config under <code>environments: []</code></div></div>'; return; }
6374
+ if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured</div>'; return; }
6448
6375
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6449
6376
  <thead><tr style="color:var(--text-xs);text-align:left">
6450
6377
  <th style="padding:5px 8px">Name</th><th>Driver</th><th>Host</th><th>Status</th>
@@ -6464,7 +6391,7 @@ function v2RenderOrgAccess() {
6464
6391
  const el = document.getElementById('odt-access');
6465
6392
  if (!el || !_v2OrgData) return;
6466
6393
  const members = _v2OrgData._members || [];
6467
- if (!members.length) { el.innerHTML = '<div class="empty">No members found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Members are added when agents join the org. See the Members tab to manage roles.</div></div>'; return; }
6394
+ if (!members.length) { el.innerHTML = '<div class="empty">No members found</div>'; return; }
6468
6395
  const TIER = { owner: 0, admin: 1, operator: 2, viewer: 3 };
6469
6396
  const byRole = {};
6470
6397
  members.forEach(m => { const r = m.role || 'viewer'; (byRole[r] || (byRole[r] = [])).push(m); });
@@ -6486,7 +6413,7 @@ function v2RenderOrgIssuesFull() {
6486
6413
  const el = document.getElementById('odt-issues-full');
6487
6414
  if (!el || !_v2OrgData) return;
6488
6415
  const issues = _v2OrgData._issues || [];
6489
- if (!issues.length) { el.innerHTML = '<div class="empty">No issues found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Issues are created by agents during task execution. Check the Board for status.</div></div>'; return; }
6416
+ if (!issues.length) { el.innerHTML = '<div class="empty">No issues found</div>'; return; }
6490
6417
  const PRIORITY = { urgent:'🔴', high:'🟠', medium:'🟡', low:'🟢' };
6491
6418
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6492
6419
  <thead><tr style="color:var(--text-xs);text-align:left">
@@ -6494,10 +6421,10 @@ function v2RenderOrgIssuesFull() {
6494
6421
  </tr></thead>
6495
6422
  <tbody>${issues.slice(0,100).map(i => `<tr style="border-top:1px solid var(--border)">
6496
6423
  <td style="padding:5px 4px">${PRIORITY[i.priority]||'·'}</td>
6497
- <td style="padding:5px 8px;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(i.title||i.description||'')}">${esc((i.title||i.description||'—').slice(0,80))}${(i.title||i.description||'').length > 80 ? '…' : ''}</td>
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>
6498
6425
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(i.assignee||'—')}</td>
6499
6426
  <td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'?'on':i.status==='blocked'?'warn':''}">${esc(i.status||'open')}</span></td>
6500
- <td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(i.updated_at||i.ts)}">${relTime(i.updated_at||i.ts)}</td>
6427
+ <td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px">${relTime(i.updated_at||i.ts)}</td>
6501
6428
  </tr>`).join('')}</tbody>
6502
6429
  </table>`;
6503
6430
  }
@@ -6511,7 +6438,7 @@ async function v2RenderOrgJoinRequests() {
6511
6438
  const _enc = encodeURIComponent(_v2SelOrg);
6512
6439
  const data = await fetch(`/api/org/${_enc}/join-requests${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6513
6440
  const reqs = Array.isArray(data) ? data : (data.joinRequests || data.join_requests || []);
6514
- if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Join requests appear when agents or users request access to this org.</div></div>'; return; }
6441
+ if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests</div>'; return; }
6515
6442
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6516
6443
  <thead><tr style="color:var(--text-xs);text-align:left">
6517
6444
  <th style="padding:5px 8px">Requester</th><th>Type</th><th>Status</th><th>Created</th>
@@ -6520,7 +6447,7 @@ async function v2RenderOrgJoinRequests() {
6520
6447
  <td style="padding:5px 8px;color:var(--text-hi)">${esc(r.name||r.username||r.id||'—')}</td>
6521
6448
  <td style="padding:5px 8px;color:var(--text-lo)">${r.type==='agent'?'🤖 agent':'👤 human'}</td>
6522
6449
  <td style="padding:5px 8px"><span class="ss-pill ${r.status==='approved'?'on':r.status==='rejected'?'warn':''}">${esc(r.status||'pending')}</span></td>
6523
- <td style="padding:5px 8px;color:var(--text-lo)" title="${r.createdAt ? new Date(typeof r.createdAt === 'number' ? r.createdAt : Number(r.createdAt) || r.createdAt).toLocaleString() : ''}">${r.createdAt ? relTime(r.createdAt) : '—'}</td>
6450
+ <td style="padding:5px 8px;color:var(--text-lo)">${r.createdAt ? relTime(r.createdAt) : '—'}</td>
6524
6451
  </tr>`).join('')}</tbody>
6525
6452
  </table>`;
6526
6453
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6535,17 +6462,17 @@ async function v2RenderOrgThreads() {
6535
6462
  const _enc = encodeURIComponent(_v2SelOrg);
6536
6463
  const data = await fetch(`/api/org/${_enc}/threads${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6537
6464
  const threads = Array.isArray(data) ? data : (data.threads || []);
6538
- if (!threads.length) { el.innerHTML = '<div class="empty">No threads found<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Threads are created when agents discuss issues. Check the Issues tab to start a discussion.</div></div>'; return; }
6465
+ if (!threads.length) { el.innerHTML = '<div class="empty">No threads found</div>'; return; }
6539
6466
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
6540
6467
  <thead><tr style="color:var(--text-xs);text-align:left">
6541
6468
  <th style="padding:5px 8px">Subject</th><th>Author</th><th>Messages</th><th>Issue</th><th>Created</th>
6542
6469
  </tr></thead>
6543
6470
  <tbody>${threads.map(t => `<tr style="border-top:1px solid var(--border)">
6544
- <td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.subject||t.title||'')}">${esc((t.subject||t.title||'—').slice(0,50))}${(t.subject||t.title||'').length > 50 ? '…' : ''}</td>
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>
6545
6472
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(t.author||t.createdBy||'—')}</td>
6546
6473
  <td style="padding:5px 8px;color:var(--text-lo);text-align:center">${t.messageCount ?? t.messages ?? '—'}</td>
6547
- <td style="padding:5px 8px;color:var(--text-lo);font-family:var(--mono);font-size:11px" title="${t.issueId ? esc(t.issueId.toString()) : ''}">${t.issueId ? '#'+esc(t.issueId.toString().slice(0,8))+(t.issueId.toString().length > 8 ? '…' : '') : '—'}</td>
6548
- <td style="padding:5px 8px;color:var(--text-lo)" title="${t.createdAt ? new Date(typeof t.createdAt === 'number' ? t.createdAt : Number(t.createdAt) || t.createdAt).toLocaleString() : ''}">${t.createdAt ? relTime(t.createdAt) : '—'}</td>
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>
6549
6476
  </tr>`).join('')}</tbody>
6550
6477
  </table>`;
6551
6478
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -6555,103 +6482,55 @@ async function v2RenderOrgThreads() {
6555
6482
  window.v2StopOrg = async function() {
6556
6483
  if (!_v2SelOrg) return;
6557
6484
  const stopped = _v2SelOrg;
6558
- try {
6559
- await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'});
6560
- showToast('Stopped', `Org "${stopped}" stopped`, 'ok');
6561
- } catch(e) { showToast('Error', e.message, 'err'); }
6485
+ try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'}); } catch(_) {}
6562
6486
  setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
6563
6487
  };
6564
6488
 
6565
- // ── Copy org dialog ────────────────────────────────────────────────────────────
6566
- window.v2ShowCopyOrgDialog = async function() {
6567
- if (!_v2SelOrg) return;
6568
- const dialog = document.getElementById('org-copy-dialog');
6569
- const feedback = document.getElementById('org-copy-feedback');
6570
- const select = document.getElementById('org-copy-dest-select');
6571
- const input = document.getElementById('org-copy-dest-input');
6572
- if (!dialog) return;
6573
- feedback.textContent = '';
6574
- input.value = '';
6575
- select.innerHTML = '<option value="">Loading…</option>';
6576
- dialog.style.display = 'flex';
6577
- // Populate project list
6578
- try {
6579
- const data = await apiFetch('/api/projects');
6580
- const projects = (data.projects || []).filter(p => p.path && p.path !== DIR);
6581
- select.innerHTML = '<option value="">— select a project —</option>' +
6582
- projects.map(p => `<option value="${esc(p.path)}">${esc(p.name)} (${esc(p.path)})</option>`).join('') +
6583
- '<option value="__custom__">Custom path…</option>';
6584
- } catch(e) {
6585
- select.innerHTML = '<option value="__custom__">Enter path manually</option>';
6586
- }
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(); }
6587
6492
  };
6588
-
6589
- window.v2HideCopyOrgDialog = function() {
6590
- const dialog = document.getElementById('org-copy-dialog');
6591
- if (dialog) dialog.style.display = 'none';
6592
- };
6593
-
6594
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; }
6595
6496
  if (!_v2SelOrg) return;
6596
- const select = document.getElementById('org-copy-dest-select');
6597
- const input = document.getElementById('org-copy-dest-input');
6598
- const feedback = document.getElementById('org-copy-feedback');
6599
- const btn = document.getElementById('org-copy-confirm-btn');
6600
- const destination = (input.value.trim()) || (select.value !== '__custom__' ? select.value : '');
6601
- if (!destination) {
6602
- feedback.textContent = 'Please select or enter a destination.';
6603
- feedback.style.color = 'var(--red, #e55)';
6604
- return;
6605
- }
6606
- btn.disabled = true;
6607
- btn.textContent = 'Copying…';
6608
- feedback.textContent = '';
6609
6497
  try {
6610
- const r = await fetch(`/api/orgs/${encodeURIComponent(_v2SelOrg)}/copy`, {
6611
- method: 'POST',
6612
- headers: { 'Content-Type': 'application/json' },
6613
- body: JSON.stringify({ destination }),
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 }),
6614
6501
  });
6615
- const json = await r.json();
6616
- if (r.ok && json.ok) {
6617
- feedback.textContent = '✓ Copied successfully to ' + destination;
6618
- feedback.style.color = 'var(--green, #4ade80)';
6619
- setTimeout(v2HideCopyOrgDialog, 1800);
6620
- } else {
6621
- feedback.textContent = 'Error: ' + (json.error || r.status);
6622
- feedback.style.color = 'var(--red, #e55)';
6623
- }
6624
- } catch(e) {
6625
- feedback.textContent = 'Error: ' + e.message;
6626
- feedback.style.color = 'var(--red, #e55)';
6627
- } finally {
6628
- btn.disabled = false;
6629
- btn.textContent = 'Copy';
6630
- }
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'); }
6631
6506
  };
6632
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
+
6633
6525
  // live SSE for org events
6634
6526
  (function v2OrgSSE() {
6635
6527
  let src;
6636
- let _connectTime = 0;
6637
6528
  function connect() {
6638
6529
  if (src) src.close();
6639
- _connectTime = Date.now();
6640
6530
  src = new EventSource('/api/mastermind-stream');
6641
6531
  src.onmessage = e => {
6642
6532
  try {
6643
6533
  const ev = JSON.parse(e.data);
6644
- if (ev?.type?.startsWith('loop:')) {
6645
- // Skip replayed historical events (server replays last 50 on connect)
6646
- if (ev.ts && ev.ts < _connectTime) return;
6647
- if (currentView === 'loops') renderLoops();
6648
- else viewRendered['loops'] = false; // stale — re-fetch on next switchView
6649
- loadLoopMetrics();
6650
- if (_mmCurrentTab === 'loops' && document.getElementById('mm-overlay')?.classList.contains('open')) {
6651
- mmRenderLoops(document.getElementById('mm-body'));
6652
- }
6653
- return;
6654
- }
6655
6534
  if (!ev?.org || !ev?.type?.startsWith('org:')) return;
6656
6535
  const n = ev.org;
6657
6536
  // Filter by project dir if the event carries one; skip events from other projects.
@@ -6720,12 +6599,11 @@ function buildTimeline(events) {
6720
6599
  // Only tool + user events with timestamps
6721
6600
  const stamped = events.filter(ev => ev.ts && (ev.kind === 'tool' || ev.kind === 'user'));
6722
6601
  if (stamped.length < 2) { tl.innerHTML = ''; return; }
6723
- const tsMs = ev => typeof ev.ts === 'number' ? ev.ts : Number(ev.ts) || 0;
6724
- const times = stamped.map(tsMs);
6602
+ const times = stamped.map(ev => new Date(ev.ts).getTime());
6725
6603
  const tMin = Math.min(...times), tMax = Math.max(...times);
6726
6604
  const span = tMax - tMin || 1;
6727
6605
  const segs = stamped.map(ev => {
6728
- const pct = ((tsMs(ev) - tMin) / span * 100).toFixed(2);
6606
+ const pct = ((new Date(ev.ts).getTime() - tMin) / span * 100).toFixed(2);
6729
6607
  const cat = ev.kind === 'user' ? 'user' : (ev.cat || 'other');
6730
6608
  const color = TL_COLORS[cat] || TL_COLORS.other;
6731
6609
  const label = ev.kind === 'user' ? 'user message' : (ev.label || ev.name || cat);
@@ -6820,13 +6698,12 @@ async function copySession() {
6820
6698
  try {
6821
6699
  const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
6822
6700
  const events = data.events || [];
6823
- const _sessTs = sess.lastTs || sess.mtime;
6824
- const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(typeof _sessTs === 'number' ? _sessTs : Number(_sessTs) || _sessTs).toLocaleString()}`, ''];
6701
+ const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`, ''];
6825
6702
  for (const ev of events) {
6826
6703
  if (ev.kind === 'user' && ev.text?.trim()) {
6827
6704
  lines.push('**User:** ' + ev.text.trim().replace(/\n/g, ' '));
6828
6705
  } else if (ev.kind === 'tool') {
6829
- lines.push(`- \`${ev.name || ev.cat}\`${ev.label ? ': ' + ev.label : ''}`);
6706
+ lines.push(`- \`${ev.name || ev.cat}\`: ${ev.label || ''}`);
6830
6707
  }
6831
6708
  }
6832
6709
  await navigator.clipboard.writeText(lines.join('\n'));
@@ -6893,10 +6770,7 @@ function cmdSearch(q) {
6893
6770
  results.innerHTML = sq.length >= 2
6894
6771
  ? '<div class="cmd-empty">Searching sessions…</div>'
6895
6772
  : '<div class="cmd-empty">Type at least 2 chars after &gt; to search all sessions</div>';
6896
- if (sq.length >= 2) {
6897
- clearTimeout(cmdSearch._debounce);
6898
- cmdSearch._debounce = setTimeout(() => searchSessions(sq), 300);
6899
- }
6773
+ if (sq.length >= 2) searchSessions(sq);
6900
6774
  return;
6901
6775
  }
6902
6776
 
@@ -6925,20 +6799,10 @@ function cmdSearch(q) {
6925
6799
 
6926
6800
  // ACTIONS group
6927
6801
  const actionItems = [
6928
- { label: 'Go to Now', action: () => switchView('now') },
6929
- { label: 'Go to Sessions', action: () => switchView('sessions') },
6930
- { label: 'Go to Projects', action: () => switchView('projects') },
6931
- { label: 'Go to Loops', action: () => switchView('loops') },
6932
- { label: 'Go to Tokens', action: () => switchView('tokens') },
6933
- { label: 'Go to Memory', action: () => switchView('memory') },
6934
- { label: 'Go to Orgs', action: () => switchView('orgs') },
6935
- { label: 'Go to Monograph', action: () => switchView('monograph') },
6936
- { label: 'Go to Global Feed',action: () => switchView('global') },
6802
+ { label: 'Open Monograph', action: () => { const l = document.querySelector('.nav-item[data-view="monograph"]'); if (l) l.click(); } },
6937
6803
  { label: 'Refresh Dashboard', action: () => refreshCurrent() },
6938
6804
  { label: 'Toggle Compact View', action: () => toggleDensity() },
6939
- { label: 'Toggle Ambient Mode', action: () => toggleAmbient() },
6940
- { label: 'Open Mastermind', action: () => openMastermind() },
6941
- { label: 'Keyboard Shortcuts', action: () => { closeCmdPalette(); openShortcutHelp(); } },
6805
+ { label: 'Open Loops', action: () => { const l = document.querySelector('.nav-item[data-view="loops"]'); if (l) l.click(); } },
6942
6806
  ].filter(a => !lq || a.label.toLowerCase().includes(lq));
6943
6807
 
6944
6808
  // TABS group — only if org room is open
@@ -6964,8 +6828,8 @@ function cmdSearch(q) {
6964
6828
  html += `<div class="cmd-item" data-ci="${idx}">
6965
6829
  <span class="ci-ico">◫</span>
6966
6830
  <div class="cmd-item-body">
6967
- <div class="ci-title" title="${esc(s.lastPrompt || s.id || '')}">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
6968
- <div class="ci-sub" title="${(ts => ts ? new Date(typeof ts === 'number' ? ts : Number(ts) || ts).toLocaleString() : '')(s.lastTs || s.mtime)}">${relTime(s.lastTs || s.mtime)}</div>
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>
6969
6833
  </div>
6970
6834
  </div>`;
6971
6835
  });
@@ -6980,7 +6844,7 @@ function cmdSearch(q) {
6980
6844
  <span class="ci-ico">◈</span>
6981
6845
  <div class="cmd-item-body">
6982
6846
  <div class="ci-title">${esc(d.key || d.namespace || '—')}</div>
6983
- <div class="ci-sub" title="${esc(String(d.value || d.text || ''))}">${esc(String(d.value || d.text || '').slice(0, 60))}${String(d.value || d.text || '').length > 60 ? '…' : ''}</div>
6847
+ <div class="ci-sub">${esc(String(d.value || d.text || '').slice(0, 60))}</div>
6984
6848
  </div>
6985
6849
  </div>`;
6986
6850
  });
@@ -7026,7 +6890,7 @@ function cmdSearch(q) {
7026
6890
  <span class="ci-ico">✦</span>
7027
6891
  <div class="cmd-item-body">
7028
6892
  <div class="ci-title">${esc(skLabel)}</div>
7029
- <div class="ci-sub" title="${typeof sk === 'string' ? '' : esc(sk.description || '')}">${typeof sk === 'string' ? '' : esc((sk.description || '').slice(0, 60)) + ((sk.description || '').length > 60 ? '…' : '')}</div>
6893
+ <div class="ci-sub">${typeof sk === 'string' ? '' : esc((sk.description || '').slice(0, 60))}</div>
7030
6894
  </div>
7031
6895
  </div>`;
7032
6896
  });
@@ -7093,7 +6957,7 @@ function executeCmdItem() {
7093
6957
  else if (item.type === 'memory') switchView('memory');
7094
6958
  else if (item.type === 'project') switchProject(item.data.path);
7095
6959
  else if (item.type === 'org') { switchView('orgs'); setTimeout(() => v2SelectOrg(item.data.name), 80); }
7096
- else if (item.type === 'skill') { const _skN = typeof item.data === 'string' ? item.data : (item.data.name || item.data.id || ''); _mmSkillFilter = _skN; openMastermind(); mmSwitchTab('skills'); }
6960
+ else if (item.type === 'skill') switchView('memory');
7097
6961
  else if (item.type === 'action') { if (item.data.action) item.data.action(); }
7098
6962
  else if (item.type === 'orgtab') { const btn = document.querySelector(`.odt-btn[data-tab="${item.data.tab}"]`); if (btn) btn.click(); }
7099
6963
  }
@@ -7116,8 +6980,8 @@ async function searchSessions(q) {
7116
6980
  html += `<div class="cmd-item" data-ci="${idx}">
7117
6981
  <span class="ci-ico">◫</span>
7118
6982
  <div class="cmd-item-body">
7119
- <div class="ci-title" title="${esc(r.lastPrompt || r.id || '')}">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
7120
- <div class="ci-sub" title="${esc(snippet)}">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
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>
7121
6985
  </div>
7122
6986
  </div>`;
7123
6987
  });
@@ -7142,22 +7006,21 @@ document.addEventListener('keydown', e => {
7142
7006
  return;
7143
7007
  }
7144
7008
 
7145
- // Escape always closes modals, even when focused inside an input/textarea
7146
- if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); closeBudgetModal(); closeChunkModal(); closeMemModal(); closeReportCard(); closeMastermind(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); return; }
7147
-
7148
7009
  // ignore when typing in inputs
7149
7010
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
7150
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(); }
7151
7014
  if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
7152
- if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); return; }
7153
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; }
7154
7018
  if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
7155
- const _viewKeys = {'1':'now','2':'sessions','3':'projects','4':'loops','5':'tokens','6':'memory','7':'orgs','8':'monograph','9':'global'};
7156
- if (_viewKeys[e.key] && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); switchView(_viewKeys[e.key]); return; }
7157
7019
 
7158
7020
  if (currentView === 'now') {
7159
7021
  if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
7160
7022
  if (e.key === 'g' || e.key === 'G') { e.preventDefault(); goLive(); }
7023
+ if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); }
7161
7024
 
7162
7025
  if (e.key === 'j' || e.key === 'k') {
7163
7026
  e.preventDefault();
@@ -7225,22 +7088,6 @@ function filterSessions(q) {
7225
7088
  });
7226
7089
  const countEl = document.getElementById('sess-filter-count');
7227
7090
  if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
7228
- // zero-results empty state
7229
- const noRes = document.getElementById('sess-filter-noresult');
7230
- if (lq && visible === 0 && rows.length > 0) {
7231
- if (!noRes) {
7232
- const el = document.createElement('div');
7233
- el.id = 'sess-filter-noresult';
7234
- el.className = 'empty';
7235
- el.style.cssText = 'padding:24px 0;font-size:13px';
7236
- el.textContent = 'No sessions match "' + q.slice(0, 40) + '"';
7237
- document.getElementById('sess-content').appendChild(el);
7238
- } else {
7239
- noRes.textContent = 'No sessions match "' + q.slice(0, 40) + '"';
7240
- }
7241
- } else if (noRes) {
7242
- noRes.remove();
7243
- }
7244
7091
  }
7245
7092
 
7246
7093
  // ── feature 32: keyboard shortcut help modal ──────────────
@@ -7267,7 +7114,7 @@ function updateCurrentActivity(events) {
7267
7114
  } else if (name) {
7268
7115
  activity = name;
7269
7116
  }
7270
- if (activity) { el.textContent = '⤷ ' + activity; el.title = recent.label || name || ''; el.classList.add('loaded'); }
7117
+ if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
7271
7118
  else { el.textContent = ''; el.classList.remove('loaded'); }
7272
7119
  }
7273
7120
 
@@ -7291,8 +7138,7 @@ function buildPatterns() {
7291
7138
  if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
7292
7139
  }
7293
7140
  }
7294
- const allTerms = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]);
7295
- 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);
7296
7142
  if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
7297
7143
  const maxCount = sorted[0][1];
7298
7144
  const rows = sorted.map(([word, count], i) => {
@@ -7305,8 +7151,7 @@ function buildPatterns() {
7305
7151
  }).join('');
7306
7152
  el.innerHTML = `<table class="lb-table"><thead><tr>
7307
7153
  <th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
7308
- </tr></thead><tbody>${rows}</tbody></table>` +
7309
- (allTerms.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${allTerms.length} terms</div>` : '');
7154
+ </tr></thead><tbody>${rows}</tbody></table>`;
7310
7155
  }
7311
7156
 
7312
7157
  // ── feature 35: session streak tracker ────────────────────
@@ -7314,7 +7159,7 @@ function calcStreak() {
7314
7159
  const dates = new Set(allSessions.map(s => {
7315
7160
  const t = s.firstTs || s.mtime;
7316
7161
  if (!t) return null;
7317
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString();
7162
+ return new Date(typeof t === 'number' ? t : t).toDateString();
7318
7163
  }).filter(Boolean));
7319
7164
  let streak = 0;
7320
7165
  const today = new Date();
@@ -7332,16 +7177,14 @@ function calcStreak() {
7332
7177
 
7333
7178
  // ── feature 25: notification toasts ──────────────────────
7334
7179
  let _toastLastBudgetKey = '';
7335
- let _toastLastKey = ''; let _toastLastTs = 0;
7336
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();
7337
7186
  const rack = document.getElementById('toast-rack');
7338
7187
  if (!rack) return;
7339
- // Dedup: skip identical toast within 2s
7340
- const key = type + '|' + title + '|' + msg;
7341
- if (key === _toastLastKey && Date.now() - _toastLastTs < 2000) return;
7342
- _toastLastKey = key; _toastLastTs = Date.now();
7343
- // Cap at 5 visible toasts — evict oldest
7344
- while (rack.children.length >= 5) rack.firstChild.remove();
7345
7188
  const icoMap = { warn: '⚑', err: '⚠', ok: '✓', info: '◉' };
7346
7189
  const div = document.createElement('div');
7347
7190
  div.className = 'toast t-' + type;
@@ -7350,7 +7193,7 @@ function showToast(title, msg, type = 'info', duration = 5000) {
7350
7193
  <div class="toast-title">${esc(title)}</div>
7351
7194
  <div class="toast-msg">${esc(msg)}</div>
7352
7195
  </div>
7353
- <button class="toast-close" onclick="this.closest('.toast').remove()" title="Dismiss">✕</button>`;
7196
+ <button class="toast-close" onclick="this.closest('.toast').remove()">✕</button>`;
7354
7197
  rack.appendChild(div);
7355
7198
  if (duration > 0) setTimeout(() => { try { div.remove(); } catch {} }, duration);
7356
7199
  }
@@ -7421,11 +7264,7 @@ function buildSessionHeatmap(sessions) {
7421
7264
  if (idx >= 0 && idx < DAYS) buckets[idx].count++;
7422
7265
  }
7423
7266
  const max = Math.max(...buckets.map(b => b.count), 1);
7424
- // pad so column 0 starts on Monday
7425
- const firstDowShm = new Date(now - (DAYS - 1) * DAY).getDay(); // 0=Sun
7426
- const shmOffset = firstDowShm === 0 ? 6 : firstDowShm - 1;
7427
- const padShm = Array.from({ length: shmOffset }, () => '<div class="shm-cell" style="opacity:0;pointer-events:none"></div>');
7428
- el.innerHTML = padShm.join('') + buckets.map(b => {
7267
+ el.innerHTML = buckets.map(b => {
7429
7268
  const level = b.count === 0 ? 0 : Math.min(4, Math.ceil(b.count / max * 4));
7430
7269
  const isActive = b.date === heatmapDateFilter;
7431
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>`;
@@ -7519,8 +7358,7 @@ function bulkExport() {
7519
7358
  if (!toExport.length) return;
7520
7359
  const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'Files Touched'];
7521
7360
  const rows = toExport.map(s => {
7522
- const _ts0 = s.firstTs || s.mtime || 0;
7523
- const dt = new Date(typeof _ts0 === 'number' ? _ts0 : Number(_ts0) || _ts0).toISOString().slice(0, 19).replace('T', ' ');
7361
+ const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
7524
7362
  const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
7525
7363
  const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
7526
7364
  const prompt = (s.lastPrompt || '').replace(/"/g, '""');
@@ -7574,8 +7412,7 @@ function exportSessionsCSV() {
7574
7412
  if (!allSessions.length) { showToast('No data', 'No sessions loaded yet', 'warn'); return; }
7575
7413
  const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'User Messages', 'Cache Hit %', 'Input Tokens'];
7576
7414
  const rows = allSessions.map(s => {
7577
- const _ts1 = s.firstTs || s.mtime || 0;
7578
- const dt = new Date(typeof _ts1 === 'number' ? _ts1 : Number(_ts1) || _ts1).toISOString().slice(0, 19).replace('T', ' ');
7415
+ const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
7579
7416
  const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
7580
7417
  const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
7581
7418
  const cachePct = s.totalInputTokens > 0 ? Math.round((s.cacheReadTokens || 0) / s.totalInputTokens * 100) : '';
@@ -7608,12 +7445,11 @@ async function loadToolRank() {
7608
7445
  const data = await apiFetch('/api/tool-ranking?dir=' + enc(DIR));
7609
7446
  if (!data.tools?.length) { el.innerHTML = '<div class="loading-txt">No tool usage data</div>'; return; }
7610
7447
  const maxCount = data.tools[0].count;
7611
- const shownTools = data.tools.slice(0, 15);
7612
- const rows = shownTools.map((t, i) => {
7448
+ const rows = data.tools.slice(0, 15).map((t, i) => {
7613
7449
  const barW = Math.round((t.count / maxCount) * 100);
7614
7450
  const errRate = t.errors > 0 ? ((t.errors / t.count) * 100).toFixed(0) + '%' : '—';
7615
7451
  return `<tr><td class="lb-rank">${i + 1}</td>
7616
- <td style="font-size:12px;color:var(--text-mid);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.tool)}">${esc(t.tool)}</td>
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>
7617
7453
  <td style="width:80px;padding:4px 6px">
7618
7454
  <div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
7619
7455
  <div style="height:100%;width:${barW}%;background:oklch(65% 0.15 200);border-radius:2px"></div>
@@ -7625,8 +7461,7 @@ async function loadToolRank() {
7625
7461
  }).join('');
7626
7462
  el.innerHTML = `<table class="lb-table"><thead><tr>
7627
7463
  <th class="lb-rank">#</th><th>Tool</th><th></th><th class="lb-cost">Calls</th><th class="lb-dur">Error%</th>
7628
- </tr></thead><tbody>${rows}</tbody></table>` +
7629
- (data.tools.length > 15 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 15 of ${data.tools.length} tools</div>` : '');
7464
+ </tr></thead><tbody>${rows}</tbody></table>`;
7630
7465
  } catch (err) {
7631
7466
  el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
7632
7467
  }
@@ -7648,14 +7483,13 @@ async function loadProjCosts() {
7648
7483
  const data = await apiFetch('/api/project-costs');
7649
7484
  if (!data.projects?.length) { el.innerHTML = '<div class="loading-txt">No cost data across projects</div>'; return; }
7650
7485
  const maxCost = data.projects[0].cost;
7651
- const shownProjects = data.projects.slice(0, 10);
7652
- const rows = shownProjects.map((p, i) => {
7486
+ const rows = data.projects.slice(0, 10).map((p, i) => {
7653
7487
  const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
7654
7488
  const name = p.path.split('/').filter(Boolean).pop() || p.path;
7655
7489
  return `<tr onclick="switchProject('${esc(p.path)}')" style="cursor:pointer" title="${esc(p.path)}">
7656
7490
  <td class="lb-rank">${i + 1}</td>
7657
7491
  <td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
7658
- <td class="lb-cost">$${(p.cost || 0).toFixed(2)}</td>
7492
+ <td class="lb-cost">$${p.cost.toFixed(2)}</td>
7659
7493
  <td style="width:80px;padding:4px 6px">
7660
7494
  <div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
7661
7495
  <div style="height:100%;width:${barW}%;background:oklch(72% 0.18 75 / 0.7);border-radius:2px"></div>
@@ -7666,8 +7500,7 @@ async function loadProjCosts() {
7666
7500
  }).join('');
7667
7501
  el.innerHTML = `<table class="lb-table"><thead><tr>
7668
7502
  <th class="lb-rank">#</th><th>Project</th><th class="lb-cost">Cost</th><th></th><th class="lb-dur">Sessions</th>
7669
- </tr></thead><tbody>${rows}</tbody></table>` +
7670
- (data.projects.length > 10 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 10 of ${data.projects.length} projects</div>` : '');
7503
+ </tr></thead><tbody>${rows}</tbody></table>`;
7671
7504
  } catch (err) {
7672
7505
  el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
7673
7506
  }
@@ -7695,7 +7528,7 @@ function buildGanttTimeline() {
7695
7528
  }
7696
7529
  for (const s of allSessions) {
7697
7530
  const t = s.firstTs || s.mtime; if (!t) continue;
7698
- 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);
7699
7532
  const key = d.toDateString();
7700
7533
  if (key in days) days[key].push(s);
7701
7534
  }
@@ -7713,11 +7546,11 @@ function buildGanttTimeline() {
7713
7546
  // Sort sessions by start time
7714
7547
  const sorted = [...sessions].sort((a, b) => {
7715
7548
  const ta = a.firstTs || a.mtime || 0; const tb = b.firstTs || b.mtime || 0;
7716
- 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());
7717
7550
  });
7718
7551
  for (let i = 0; i < sorted.length; i++) {
7719
7552
  const s = sorted[i];
7720
- 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();
7721
7554
  const dayStart = new Date(dateStr).getTime();
7722
7555
  const startPct = Math.min(95, ((startTs - dayStart) / DAY) * 100);
7723
7556
  const durPct = s.totalDurationMs ? Math.max(0.5, Math.min(15, (s.totalDurationMs / DAY) * 100)) : 1;
@@ -7744,7 +7577,7 @@ function showReportCard() {
7744
7577
 
7745
7578
  const todaySess = allSessions.filter(s => {
7746
7579
  const t = s.firstTs || s.mtime; if (!t) return false;
7747
- return new Date(typeof t === 'number' ? t : Number(t) || t).toDateString() === todayStr;
7580
+ return new Date(typeof t === 'number' ? t : t).toDateString() === todayStr;
7748
7581
  });
7749
7582
  const weekSess = allSessions.filter(s => {
7750
7583
  const t = s.firstTs || s.mtime; if (!t) return false;
@@ -7894,7 +7727,7 @@ function showCostExplainer(sessId, event) {
7894
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('');
7895
7728
  drawer.innerHTML = `<div class="err-drawer-head" style="color:oklch(70% 0.18 80)">
7896
7729
  <span>Cost anomaly — $${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
7897
- <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>
7898
7731
  </div>
7899
7732
  <div class="err-drawer-body">
7900
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>
@@ -7963,7 +7796,7 @@ function buildHourlyHeatmap() {
7963
7796
  const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
7964
7797
  for (const s of allSessions) {
7965
7798
  const t = s.firstTs || s.mtime; if (!t) continue;
7966
- const d = new Date(typeof t === 'number' ? t : Number(t) || t);
7799
+ const d = new Date(typeof t === 'number' ? t : t);
7967
7800
  const dow = d.getDay(); // 0=Sun
7968
7801
  const hour = d.getHours();
7969
7802
  grid[dow][hour]++;
@@ -7994,7 +7827,7 @@ function buildHourlyHeatmap() {
7994
7827
 
7995
7828
  // ── feature 50: custom tag editor ─────────────────────────
7996
7829
  const _customTagsKey = 'mm-custom-tags';
7997
- 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(); }
7998
7831
 
7999
7832
  function getCustomTags(sessId) {
8000
7833
  return _customTagsMap.get(sessId) || [];
@@ -8014,12 +7847,10 @@ function addCustomTag(sessId, tag, event) {
8014
7847
  if (!tags.includes(t)) { tags.push(t); saveCustomTags(sessId, tags); }
8015
7848
  const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
8016
7849
  if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
8017
- // 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
8018
7851
  initTags();
8019
7852
  const tfBar = document.querySelector('.tag-filter-bar');
8020
- const newBarHtml = buildTagFilterBar(allSessions);
8021
- if (tfBar) tfBar.outerHTML = newBarHtml;
8022
- else if (newBarHtml) renderSessions();
7853
+ if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
8023
7854
  }
8024
7855
 
8025
7856
  function removeCustomTag(sessId, tag, event) {
@@ -8028,9 +7859,10 @@ function removeCustomTag(sessId, tag, event) {
8028
7859
  saveCustomTags(sessId, tags);
8029
7860
  const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
8030
7861
  if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
8031
- initTags();
8032
7862
  const tfBar = document.querySelector('.tag-filter-bar');
8033
- if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
7863
+ const newBarHtml = buildTagFilterBar(allSessions);
7864
+ if (tfBar) tfBar.outerHTML = newBarHtml;
7865
+ else if (newBarHtml) renderSessions();
8034
7866
  }
8035
7867
 
8036
7868
  function showCustomTagInput(sessId, event) {
@@ -8043,7 +7875,7 @@ function showCustomTagInput(sessId, event) {
8043
7875
  iw.className = 'ctag-input-wrap';
8044
7876
  iw.onclick = e => e.stopPropagation();
8045
7877
  iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
8046
- <button class="ctag-ok" title="Add this tag to the session" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
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>`;
8047
7879
  wrap.appendChild(iw);
8048
7880
  const inp = iw.querySelector('input');
8049
7881
  inp.focus();
@@ -8074,14 +7906,14 @@ async function toggleErrDrawer(sessId, event) {
8074
7906
  try {
8075
7907
  const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
8076
7908
  if (!data.errors?.length) {
8077
- drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')" title="Close">✕</button></div>`;
7909
+ drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
8078
7910
  return;
8079
7911
  }
8080
7912
  const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
8081
- drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')" title="Close">✕</button></div>
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>
8082
7914
  <div class="err-drawer-body">${items}</div>`;
8083
7915
  } catch (err) {
8084
- drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')" title="Close">✕</button></div>`;
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>`;
8085
7917
  }
8086
7918
  }
8087
7919
 
@@ -8115,7 +7947,7 @@ function buildCostHistogram() {
8115
7947
  const counts = new Array(BUCKETS).fill(0);
8116
7948
  for (const c of costs) { const i = Math.min(BUCKETS - 1, Math.floor((c - minC) / bucketSize)); counts[i]++; }
8117
7949
  const maxCount = Math.max(1, ...counts);
8118
- 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);
8119
7951
  const bars = counts.map((n, i) => {
8120
7952
  const h = Math.max(2, Math.round((n / maxCount) * 46));
8121
7953
  const lo = minC + i * bucketSize; const hi = lo + bucketSize;
@@ -8181,25 +8013,26 @@ function esc(s) {
8181
8013
 
8182
8014
  function relTime(ts) {
8183
8015
  if (!ts) return '';
8184
- const abs = typeof ts === 'number' ? ts : (Number(ts) || new Date(ts).getTime() || 0);
8185
- const diff = Date.now() - abs;
8016
+ const diff = Date.now() - (typeof ts === 'number' ? ts : Number(ts) || new Date(ts).getTime() || 0);
8186
8017
  const s = Math.floor(diff / 1000);
8187
8018
  if (s < 5) return 'now';
8188
- if (s < 60) return s + 's ago';
8019
+ if (s < 60) return s + 's';
8189
8020
  const m = Math.floor(s / 60);
8190
- if (m < 60) return m + 'm ago';
8021
+ if (m < 60) return m + 'm';
8191
8022
  const h = Math.floor(m / 60);
8192
- if (h < 24) return h + 'h ago';
8193
- const d = Math.floor(h / 24);
8194
- if (d < 7) return d + 'd ago';
8195
- const dt = new Date(abs);
8196
- const mon = dt.toLocaleString('default', { month: 'short' });
8197
- const sameYear = dt.getFullYear() === new Date().getFullYear();
8198
- return sameYear ? mon + ' ' + dt.getDate() : mon + ' ' + dt.getDate() + ', ' + dt.getFullYear();
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('/');
8199
8032
  }
8200
8033
 
8201
8034
  function fmtDur(ms) {
8202
- if (!ms || ms < 1000) return '<1s';
8035
+ if (ms > 0 && ms < 1000) return '<1s';
8203
8036
  const s = Math.floor(ms / 1000);
8204
8037
  if (s < 60) return s + 's';
8205
8038
  const m = Math.floor(s / 60);
@@ -8396,7 +8229,7 @@ function renderMgOverview() {
8396
8229
  topEl.innerHTML = prSorted.map(n => `
8397
8230
  <div class="mg-bar-row">
8398
8231
  <div class="mg-bar-lbl" title="${esc(n.label)}">${esc(mgShortLabel(n.label))}</div>
8399
- <div class="mg-bar-track" title="${esc(n.label)}: PageRank ${n.score.toExponential(3)}"><div class="mg-bar-fill" style="width:${maxPR ? Math.round((n.score/maxPR)*100) : 0}%"></div></div>
8232
+ <div class="mg-bar-track"><div class="mg-bar-fill" style="width:${maxPR ? Math.round((n.score/maxPR)*100) : 0}%"></div></div>
8400
8233
  <div class="mg-bar-val">${n.score.toExponential(1)}</div>
8401
8234
  </div>`).join('');
8402
8235
  }
@@ -8421,7 +8254,7 @@ function renderMgOverview() {
8421
8254
  typeEl.innerHTML = typeEntries.map(([t, c]) => `
8422
8255
  <div class="mg-bar-row">
8423
8256
  <div class="mg-bar-lbl">${esc(t)}</div>
8424
- <div class="mg-bar-track" title="${esc(t)}: ${c} node${c!==1?'s':''} (${Math.round((c/maxTC)*100)}%)"><div class="mg-bar-fill mg" style="width:${Math.round((c/maxTC)*100)}%"></div></div>
8257
+ <div class="mg-bar-track"><div class="mg-bar-fill mg" style="width:${Math.round((c/maxTC)*100)}%"></div></div>
8425
8258
  <div class="mg-bar-val">${c}</div>
8426
8259
  </div>`).join('');
8427
8260
  }
@@ -8473,11 +8306,9 @@ async function mgRebuild() {
8473
8306
  try {
8474
8307
  const res = await fetch('/api/monograph-build?dir=' + enc(DIR), { method: 'POST' });
8475
8308
  if (!res.ok) throw new Error('HTTP ' + res.status);
8476
- showToast('Rebuilding', 'Knowledge graph rebuild started', 'info');
8477
8309
  _mgLoaded = false;
8478
8310
  await loadMonograph();
8479
- showToast('Done', 'Knowledge graph rebuilt', 'ok');
8480
- } catch (e) { showToast('Error', e.message, 'err'); }
8311
+ } catch (_) {}
8481
8312
  btn.disabled = false; btn.textContent = 'REBUILD';
8482
8313
  }
8483
8314
 
@@ -8604,16 +8435,14 @@ function mgRunClient(id) {
8604
8435
  } else if (id === 'pagerank') {
8605
8436
  // Proper power-iteration PageRank (d=0.85, 50 iterations)
8606
8437
  const prMap = mgComputePageRank(g);
8607
- const allRanked = nodes.map(n => {
8438
+ const ranked = nodes.map(n => {
8608
8439
  const k = n.id || n.name || n.label || '';
8609
8440
  return { label: n.label || n.name || k, score: prMap[k] || 0 };
8610
- }).sort((a, b) => b.score - a.score);
8611
- const ranked = allRanked.slice(0, 20);
8441
+ }).sort((a, b) => b.score - a.score).slice(0, 20);
8612
8442
  const maxScore = ranked.length ? ranked[0].score : 1;
8613
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>` +
8614
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('') +
8615
- `</tbody></table>` +
8616
- (allRanked.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${allRanked.length} nodes</div>` : '');
8445
+ `</tbody></table>`;
8617
8446
 
8618
8447
  } else if (id === 'deadcode') {
8619
8448
  const isolated = nodes.filter(n => (degMap[n.id || n.name || n.label || ''] || 0) === 0);
@@ -8645,8 +8474,7 @@ function mgRunClient(id) {
8645
8474
  <div class="mg-kv-card"><div class="mkv-k">Components</div><div class="mkv-v">${comps.length}</div></div>
8646
8475
  <div class="mg-kv-card"><div class="mkv-k">Largest</div><div class="mkv-v">${comps[0] ? comps[0].length : 0}</div></div>
8647
8476
  </div>` +
8648
- top5.map((c, i) => `<div style="margin-bottom:8px"><div style="font-size:11px;color:var(--text-xs);margin-bottom:3px">Component ${i+1} — ${c.length} nodes</div><div style="font-size:11px;font-family:var(--mono);color:var(--text-mid)">${c.slice(0,5).map(esc).join(', ')}${c.length > 5 ? ` … +${c.length-5} more` : ''}</div></div>`).join('') +
8649
- (comps.length > 5 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Showing 5 of ${comps.length} components</div>` : '');
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('');
8650
8478
 
8651
8479
  } else if (id === 'topo') {
8652
8480
  const inDeg = {}; const adj = {};
@@ -8723,16 +8551,14 @@ function mgRunClient(id) {
8723
8551
 
8724
8552
  } else if (id === 'betweenness') {
8725
8553
  const bc = mgApproxBetweenness(g);
8726
- const bcAll = Object.entries(bc).sort((a,b) => b[1]-a[1]);
8727
- const bcRanked = bcAll.slice(0,20);
8554
+ const bcRanked = Object.entries(bc).sort((a,b) => b[1]-a[1]).slice(0,20);
8728
8555
  const maxBC = bcRanked.length ? bcRanked[0][1] : 1;
8729
8556
  if (!bcRanked.length || maxBC === 0) { html = '<div class="loading-txt">Not enough graph data for betweenness</div>'; }
8730
8557
  else {
8731
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>
8732
8559
  <table class="mg-table"><thead><tr><th>#</th><th>Node</th><th>Score</th><th style="width:160px">Weight</th></tr></thead><tbody>` +
8733
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('') +
8734
- `</tbody></table>` +
8735
- (bcAll.length > 20 ? `<div style="font-size:11px;color:var(--text-xs);margin-top:4px;text-align:right">Showing 20 of ${bcAll.length} nodes</div>` : '');
8561
+ `</tbody></table>`;
8736
8562
  }
8737
8563
 
8738
8564
  } else if (id === 'jaccard') {
@@ -8855,7 +8681,7 @@ function mgRenderAnalysisResult(toolId, text) {
8855
8681
  return `<div class="mgr-ranked-row">
8856
8682
  <span class="mgr-ranked-num">${r.rank}</span>
8857
8683
  <span class="mgr-ranked-name" title="${h(r.name)}">${h(r.name)}</span>
8858
- <div class="mgr-ranked-bar-wrap" title="${h(r.name)}: ${h(String(r.val))}"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
8684
+ <div class="mgr-ranked-bar-wrap"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
8859
8685
  <span class="mgr-ranked-val">${h(String(r.val))}</span>
8860
8686
  </div>`;
8861
8687
  }).join('')}</div>`;
@@ -8985,7 +8811,7 @@ function mgRenderAnalysisResult(toolId, text) {
8985
8811
  return `<div class="mgr-ranked-row">
8986
8812
  <span class="mgr-ranked-num">${r.rank}</span>
8987
8813
  <span class="mgr-ranked-name" title="${h(r.path)}">${h(name)}</span>
8988
- <div class="mgr-ranked-bar-wrap" title="${h(r.path)}: ${r.lines.toLocaleString()} lines"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
8814
+ <div class="mgr-ranked-bar-wrap"><div class="mgr-ranked-bar-fill" style="width:${pct}%"></div></div>
8989
8815
  <span class="mgr-ranked-val">${r.lines.toLocaleString()}</span>
8990
8816
  </div>`;
8991
8817
  }).join('')}</div>`;
@@ -9033,13 +8859,6 @@ async function mgRunServer(id) {
9033
8859
  }
9034
8860
 
9035
8861
  // ── QUERY TAB ──────────────────────────────────────────────
9036
- // Click any query result box to copy its text content
9037
- document.addEventListener('click', e => {
9038
- const el = e.target.closest('.mg-query-result');
9039
- if (!el || !el.textContent.trim()) return;
9040
- navigator.clipboard.writeText(el.textContent).then(() => showToast('Copied', 'Query result copied', 'ok')).catch(() => {});
9041
- });
9042
-
9043
8862
  function mgQuerySearch(q) {
9044
8863
  const res = document.getElementById('mg-q-search-result');
9045
8864
  if (!q.trim()) { res.style.display = 'none'; return; }
@@ -9234,7 +9053,7 @@ function mgRenderExport() {
9234
9053
  <div class="mg-export-card">
9235
9054
  <div class="mec-name">${esc(x.name)}</div>
9236
9055
  <div class="mec-desc">${esc(x.desc)}</div>
9237
- <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>
9238
9057
  </div>`).join('');
9239
9058
  }
9240
9059
 
@@ -9498,8 +9317,8 @@ function mgRenderWiki() {
9498
9317
  // type pills
9499
9318
  const types = [...new Set(nodes.map(n => n.type || n.kind || 'unknown'))].sort();
9500
9319
  const pillsEl = document.getElementById('mg-wiki-pills');
9501
- pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" title="Show all node types" onclick="mgWikiFilterType('all')">All</span>` +
9502
- types.map(t => `<span class="mg-pill" data-type="${esc(t)}" title="Filter by type: ${esc(t)}" onclick="mgWikiFilterType('${esc(t)}')">${esc(t)}</span>`).join('');
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('');
9503
9322
 
9504
9323
  mgRenderWikiList('');
9505
9324
  }
@@ -9570,11 +9389,9 @@ function mgWikiSearchDebounced(q) {
9570
9389
  apiFetch('/api/monograph-wiki-search?q=' + enc(q) + '&dir=' + enc(DIR))
9571
9390
  .then(d => {
9572
9391
  if (!d || !d.nodes || !d.nodes.length) return;
9573
- const serverNodes = d.nodes.slice(0, 50);
9574
- el._wikiData = serverNodes;
9575
- el.innerHTML = serverNodes.map((n, idx) => {
9392
+ el.innerHTML = d.nodes.slice(0, 50).map(n => {
9576
9393
  const lbl = n.label || n.name || '';
9577
- return `<div class="mg-node-item" onclick="mgWikiShowDetail(${idx})"><div class="mni-ico">${mgNodeIcon(n.type)}</div><div class="mni-lbl">${esc(lbl)}</div><div class="mni-path">${esc(mgShortPath(lbl))}</div><div class="mni-deg">${n.degree || 0}</div></div>`;
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>`;
9578
9395
  }).join('');
9579
9396
  }).catch(() => {});
9580
9397
  }
@@ -9599,9 +9416,9 @@ function mgWikiShowDetail(idx) {
9599
9416
  </div>
9600
9417
  ${contentPreview ? `<div class="mdp-content-preview">${esc(contentPreview)}</div>` : ''}
9601
9418
  <div class="mdp-actions">
9602
- ${contentPreview ? `<button class="btn" id="mg-copy-btn" title="Copy file content to clipboard" data-copy-idx="${idx}">COPY CONTENT</button>` : ''}
9603
- <button class="btn" title="Ask the AI to explain this node" data-explain-idx="${idx}">EXPLAIN</button>
9604
- <button class="btn" title="Find nodes related to this one in the graph" data-related-idx="${idx}">FIND RELATED</button>
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>
9605
9422
  </div>
9606
9423
  <div class="mdp-explain-result" id="mg-explain-result" style="display:none"></div>
9607
9424
  <div id="mg-related-result" style="display:none;margin-top:10px"></div>`;
@@ -9636,26 +9453,6 @@ async function mgWikiExplain(nodeId, btn) {
9636
9453
  btn.disabled = false; btn.textContent = 'EXPLAIN';
9637
9454
  }
9638
9455
 
9639
- function mgWikiJumpToNode(nodeId) {
9640
- if (!_mgGraph) return;
9641
- const el = document.getElementById('mg-wiki-list');
9642
- const data = el._wikiData || (_mgGraph.nodes || []);
9643
- const idx = data.findIndex(n => (n.id || n.name || n.label || '') === nodeId);
9644
- if (idx >= 0) {
9645
- mgWikiShowDetail(idx);
9646
- document.getElementById('mg-wiki-detail')?.scrollIntoView({ behavior:'smooth', block:'nearest' });
9647
- } else {
9648
- // node not in current filtered list — search for it and jump
9649
- const searchEl = document.getElementById('mg-wiki-search');
9650
- if (searchEl) { searchEl.value = nodeId; mgRenderWikiList(nodeId); }
9651
- setTimeout(() => {
9652
- const newData = document.getElementById('mg-wiki-list')?._wikiData || [];
9653
- const newIdx = newData.findIndex(n => (n.id || n.name || n.label || '') === nodeId);
9654
- if (newIdx >= 0) mgWikiShowDetail(newIdx);
9655
- }, 100);
9656
- }
9657
- }
9658
-
9659
9456
  function mgWikiFindRelated(nodeId) {
9660
9457
  const el = document.getElementById('mg-related-result');
9661
9458
  if (!el || !_mgGraph) return;
@@ -9671,12 +9468,11 @@ function mgWikiFindRelated(nodeId) {
9671
9468
  hop1.forEach(v => { (adj[v] ? [...adj[v]] : []).forEach(w => { if (w !== nodeId && !hop1.includes(w)) hop2.add(w); }); });
9672
9469
  const related = [...hop1, ...hop2].slice(0, 20);
9673
9470
  if (!related.length) { el.innerHTML = '<div class="loading-txt">No related nodes found</div>'; el.style.display = 'block'; return; }
9674
- 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>` +
9675
9472
  `<ul class="mg-node-list">` + related.map(k => {
9676
9473
  const n = (_mgGraph.nodes || []).find(x => (x.id || x.name || x.label || '') === k);
9677
9474
  const lbl = n ? (n.label || n.name || k) : k;
9678
- const typeIcon = n ? mgNodeIcon(n.type || n.kind) : '◈';
9679
- return `<li style="cursor:pointer;display:flex;align-items:center;gap:6px;padding:2px 0" onclick="mgWikiJumpToNode(${JSON.stringify(k)})" title="Open ${esc(lbl)}"><span style="opacity:0.6">${typeIcon}</span>${esc(lbl)}</li>`;
9475
+ return `<li>${esc(lbl)}</li>`;
9680
9476
  }).join('') + `</ul>`;
9681
9477
  el.style.display = 'block';
9682
9478
  }
@@ -9692,19 +9488,18 @@ async function mgRebuildDocs() {
9692
9488
  btn.disabled = true; btn.textContent = 'BUILDING…';
9693
9489
  try {
9694
9490
  await fetch('/api/monograph-build-docs?dir=' + enc(DIR), { method:'POST' });
9695
- showToast('Building', 'Documentation build started…', 'info');
9696
9491
  // poll until done (max 60 attempts = ~2 minutes)
9697
9492
  let polls = 0;
9698
9493
  const poll = async () => {
9699
- 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; }
9700
9495
  try {
9701
9496
  const d = await apiFetch('/api/monograph-build-docs-status?dir=' + enc(DIR));
9702
- 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; }
9703
9498
  } catch (_) {}
9704
9499
  setTimeout(poll, 2000);
9705
9500
  };
9706
9501
  poll();
9707
- } catch (e) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; showToast('Error', e.message, 'err'); }
9502
+ } catch (e) { btn.disabled = false; btn.textContent = 'BUILD DOCS'; }
9708
9503
  }
9709
9504
 
9710
9505
  // ── memory CRUD ────────────────────────────────────────────
@@ -9742,41 +9537,6 @@ async function loadMemoriesTab() {
9742
9537
  }
9743
9538
  }
9744
9539
 
9745
- function filterMemList(q) {
9746
- const lq = (q || '').toLowerCase();
9747
- let totalVisible = 0;
9748
- document.querySelectorAll('#mem-list-pane .mem-item').forEach(el => {
9749
- const text = (el.textContent || '').toLowerCase();
9750
- const show = !lq || text.includes(lq);
9751
- el.style.display = show ? '' : 'none';
9752
- if (show) totalVisible++;
9753
- });
9754
- document.querySelectorAll('#mem-list-pane .mem-type-hdr').forEach(hdr => {
9755
- let next = hdr.nextElementSibling;
9756
- let anyVisible = false;
9757
- while (next && !next.classList.contains('mem-type-hdr')) {
9758
- if (next.style.display !== 'none') anyVisible = true;
9759
- next = next.nextElementSibling;
9760
- }
9761
- hdr.style.display = anyVisible ? '' : 'none';
9762
- });
9763
- const list = document.getElementById('mem-list-pane');
9764
- const noRes = document.getElementById('mem-filter-noresult');
9765
- if (lq && totalVisible === 0 && _memFiles.length > 0) {
9766
- if (!noRes) {
9767
- const el = document.createElement('div');
9768
- el.id = 'mem-filter-noresult';
9769
- el.style.cssText = 'padding:12px 14px;color:var(--text-lo);font-size:12px';
9770
- el.textContent = 'No memories match "' + q.slice(0, 30) + '"';
9771
- list.appendChild(el);
9772
- } else {
9773
- noRes.textContent = 'No memories match "' + q.slice(0, 30) + '"';
9774
- }
9775
- } else if (noRes) {
9776
- noRes.remove();
9777
- }
9778
- }
9779
-
9780
9540
  function _renderMemList() {
9781
9541
  const list = document.getElementById('mem-list-pane');
9782
9542
  if (!list) return;
@@ -9792,8 +9552,7 @@ function _renderMemList() {
9792
9552
  const col = _MEM_COLORS[type] || _MEM_COLOR_FALLBACK;
9793
9553
  const fname = f.filename || f.name || '?';
9794
9554
  const active = _selMemFilename === fname ? ' active' : '';
9795
- const memTitle = [f.description, f.name || fname].filter(Boolean).join(' ');
9796
- return '<div class="mem-item' + active + '" data-filename="' + esc(fname) + '" onclick="selectMem(this.dataset.filename)" title="' + esc(memTitle) + '">' +
9555
+ return '<div class="mem-item' + active + '" data-filename="' + esc(fname) + '" onclick="selectMem(this.dataset.filename)">' +
9797
9556
  '<span class="mem-type-dot" style="background:' + esc(col) + ';flex-shrink:0"></span>' +
9798
9557
  '<span class="mem-item-name">' + esc(f.name || fname.replace('.md', '')) + '</span>' +
9799
9558
  '</div>';
@@ -9812,17 +9571,17 @@ function selectMem(filename) {
9812
9571
  const rawBody = f.body || f.content || '';
9813
9572
  const bodyHtml = rawBody
9814
9573
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
9815
- .replace(/\*\*(.+?)\*\*/g, (_, g) => '<strong>' + g + '</strong>')
9816
- .replace(/^#{1,3} (.+)/gm, (_, g) => '<div style="font-weight:600;color:var(--text-hi);margin:6px 0 2px">' + g + '</div>')
9817
- .replace(/^- (.+)/gm, (_, g) => '<div style="padding-left:12px">• ' + g + '</div>');
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>');
9818
9577
  const srcBadge = f.source === 'backend'
9819
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>'
9820
9579
  : '';
9821
9580
  const actions = (f.readonly || f.source === 'backend')
9822
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>'
9823
9582
  : '<div class="mem-actions">' +
9824
- '<button class="btn" title="Edit this memory entry" onclick="openEditMemModal(' + JSON.stringify(filename) + ')">&#x270E; Edit</button>' +
9825
- '<button class="btn" title="Delete this memory entry" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">&#x2715; Delete</button>' +
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>' +
9826
9585
  '</div>';
9827
9586
  detail.innerHTML =
9828
9587
  '<span class="mem-badge" style="background:' + col + '22;color:' + col + '">' + esc(f.type || '?') + '</span>' + srcBadge +
@@ -9932,7 +9691,7 @@ function _renderSwarmRunList() {
9932
9691
  '<span class="swarm-topo-pill">' + esc(topo) + '</span>' +
9933
9692
  (live ? '<span class="swarm-live">⬤ LIVE</span>' : '') +
9934
9693
  '</div>' +
9935
- '<div style="font-size:11px;color:var(--text-lo);margin-top:2px">' + (r.agentCount || 0) + ' agents · ' + (function(ts){const d=ts&&new Date(typeof ts==='number'?ts:Number(ts)||ts);return d&&!isNaN(d)?'<span title="'+d.toLocaleString()+'">'+relTime(ts)+'</span>':relTime(ts);})(r.startedAt||r.created_at) + '</div>' +
9694
+ '<div style="font-size:11px;color:var(--text-lo);margin-top:2px">' + (r.agentCount || 0) + ' agents · ' + relTime(r.startedAt || r.created_at) + '</div>' +
9936
9695
  '</div>';
9937
9696
  }).join('');
9938
9697
  }
@@ -9945,7 +9704,7 @@ async function selectSwarmRun(idx) {
9945
9704
  if (!detail) return;
9946
9705
  detail.innerHTML =
9947
9706
  '<div style="margin-bottom:10px">' +
9948
- '<div style="font-size:13px;font-weight:600;color:var(--text-hi)" title="' + esc((run.swarmId || run.id || '').toString()) + '">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
9707
+ '<div style="font-size:13px;font-weight:600;color:var(--text-hi)">' + esc((run.swarmId || run.id || '—').toString().slice(0, 14)) + '</div>' +
9949
9708
  '<div style="font-size:11px;color:var(--text-lo);margin-top:3px">' + esc(run.topology || '—') + ' · ' + esc(run.consensus || '—') + ' · ' + (run.agentCount || 0) + ' agents</div>' +
9950
9709
  '</div>' +
9951
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>' +
@@ -10022,16 +9781,14 @@ function _renderSwarmAgents(run) {
10022
9781
  if (!el) return;
10023
9782
  const agents = run.agents || [];
10024
9783
  if (!agents.length) { el.innerHTML = ''; return; }
10025
- const shownA = agents.slice(0, 15);
10026
9784
  el.innerHTML = '<div class="m-group-title" style="margin-bottom:4px">Agents</div>' +
10027
- shownA.map(a =>
9785
+ agents.slice(0, 15).map(a =>
10028
9786
  '<div style="display:flex;gap:10px;padding:3px 0;font-size:11px;border-bottom:1px solid var(--border)">' +
10029
- '<span style="color:var(--text-lo);font-family:var(--mono);min-width:70px" title="' + esc((a.id || '').toString()) + '">' + esc((a.id || '').toString().slice(0, 10)) + ((a.id || '').toString().length > 10 ? '…' : '') + '</span>' +
9787
+ '<span style="color:var(--text-lo);font-family:var(--mono);min-width:70px">' + esc((a.id || '').toString().slice(0, 10)) + '</span>' +
10030
9788
  '<span style="color:var(--text-hi)">' + esc(a.type || a.role || 'worker') + '</span>' +
10031
9789
  '<span style="margin-left:auto;color:var(--text-xs)">' + esc(a.status || '—') + '</span>' +
10032
9790
  '</div>'
10033
- ).join('') +
10034
- (agents.length > 15 ? '<div style="font-size:11px;color:var(--text-xs);padding:3px 0">+' + (agents.length - 15) + ' more agents</div>' : '');
9791
+ ).join('');
10035
9792
  }
10036
9793
 
10037
9794
  async function _loadSwarmEvents(swarmId) {
@@ -10042,13 +9799,11 @@ async function _loadSwarmEvents(swarmId) {
10042
9799
  const data = await apiFetch('/api/swarm-events?agentId=' + enc(swarmId) + '&dir=' + enc(DIR));
10043
9800
  const events = Array.isArray(data) ? data : (data.events || []);
10044
9801
  if (!events.length) { el.innerHTML = ''; return; }
10045
- const shownEv = events.slice(-40);
10046
- el.innerHTML = '<div class="m-group-title" style="margin-bottom:3px">Events' + (events.length > 40 ? '<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:6px">last 40 of ' + events.length + '</span>' : '') + '</div>' +
10047
- shownEv.map(e =>
9802
+ el.innerHTML = '<div class="m-group-title" style="margin-bottom:3px">Events</div>' +
9803
+ events.slice(-40).map(e =>
10048
9804
  '<div style="color:var(--text-lo);padding:2px 0;border-bottom:1px solid var(--border)">' +
10049
- '<span title="' + ((e.ts || e.timestamp) ? new Date(typeof (e.ts || e.timestamp) === 'number' ? (e.ts || e.timestamp) : (e.ts || e.timestamp)).toLocaleString() : '') + '">' + esc(relTime(e.ts || e.timestamp)) + '</span>' +
10050
- ' <span style="color:var(--text-mid)">' + esc(e.type || e.kind || '?') + '</span> ' +
10051
- '<span title="' + esc((e.message || e.data || '').toString()) + '">' + esc((e.message || e.data || '').toString().slice(0, 70)) + ((e.message || e.data || '').toString().length > 70 ? '…' : '') + '</span>' +
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)) +
10052
9807
  '</div>'
10053
9808
  ).join('');
10054
9809
  el.scrollTop = el.scrollHeight;
@@ -10112,25 +9867,23 @@ function _renderChunks(list) {
10112
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>';
10113
9868
  return;
10114
9869
  }
10115
- const shown = list.slice(0, 200);
10116
- grid.innerHTML = shown.map(c => {
9870
+ grid.innerHTML = list.slice(0, 200).map(c => {
10117
9871
  const src = (c.source || c.file || '').split('/').slice(-2).join('/');
10118
9872
  const excerpt = (c.content || c.text || c.body || '').slice(0, 220);
10119
9873
  const ns = c.namespace || c.type || '';
10120
- const chunkId = JSON.stringify(c.id || c.path || c.source || '').replace(/"/g, '&quot;');
10121
- const chunkContent = JSON.stringify(c.content || c.text || c.body || '').replace(/"/g, '&quot;');
10122
- const chunkSrc = JSON.stringify(src).replace(/"/g, '&quot;');
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);
10123
9877
  return '<div class="chunk-card" data-search="' + esc((src + ' ' + excerpt + ' ' + ns).toLowerCase()) + '">' +
10124
9878
  '<div class="chunk-src">' + esc(src || '—') + '</div>' +
10125
- '<div class="chunk-excerpt" title="' + esc(c.content || c.text || c.body || '') + '">' + esc(excerpt) + ((c.content || c.text || c.body || '').length > 220 ? '…' : '') + '</div>' +
9879
+ '<div class="chunk-excerpt">' + esc(excerpt) + '</div>' +
10126
9880
  '<div class="chunk-footer">' +
10127
9881
  (ns ? '<span class="chunk-ns">' + esc(ns) + '</span>' : '') +
10128
- '<button class="btn" title="Edit this chunk" style="margin-left:auto;font-size:10px;padding:1px 7px" onclick="openChunkEdit(' + chunkId + ',' + chunkContent + ',' + chunkSrc + ')">✎ Edit</button>' +
10129
- '<button class="btn" title="Delete this chunk" style="font-size:10px;padding:1px 7px;color:var(--red);border-color:var(--red)" onclick="deleteChunk(' + chunkId + ')">✕</button>' +
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>' +
10130
9884
  '</div>' +
10131
9885
  '</div>';
10132
- }).join('') +
10133
- (list.length > 200 ? '<div style="font-size:11px;color:var(--text-xs);padding:8px 0;text-align:center">Showing 200 of ' + list.length + ' chunks — use the filter to narrow results</div>' : '');
9886
+ }).join('');
10134
9887
  }
10135
9888
 
10136
9889
  function openChunkEdit(id, content, srcLabel) {
@@ -10204,33 +9957,9 @@ async function buildKnowledgeDocs() {
10204
9957
 
10205
9958
  function filterChunks(q) {
10206
9959
  const lq = q.toLowerCase();
10207
- const cards = document.querySelectorAll('#chunks-grid .chunk-card');
10208
- let visible = 0;
10209
- cards.forEach(el => {
10210
- const show = !lq || (el.dataset.search || '').includes(lq);
10211
- el.style.display = show ? '' : 'none';
10212
- if (show) visible++;
9960
+ document.querySelectorAll('#chunks-grid .chunk-card').forEach(el => {
9961
+ el.style.display = (!lq || (el.dataset.search || '').includes(lq)) ? '' : 'none';
10213
9962
  });
10214
- const countEl = document.getElementById('chunk-count-val');
10215
- if (countEl && lq) countEl.textContent = visible + ' / ' + cards.length;
10216
- else if (countEl) countEl.textContent = cards.length.toLocaleString();
10217
- // zero-results empty state
10218
- const grid = document.getElementById('chunks-grid');
10219
- const noRes = document.getElementById('chunks-filter-noresult');
10220
- if (lq && visible === 0 && cards.length > 0) {
10221
- if (!noRes) {
10222
- const el = document.createElement('div');
10223
- el.id = 'chunks-filter-noresult';
10224
- el.className = 'empty';
10225
- el.style.cssText = 'padding:20px 0;font-size:13px';
10226
- el.textContent = 'No chunks match "' + q.slice(0, 40) + '"';
10227
- grid.appendChild(el);
10228
- } else {
10229
- noRes.textContent = 'No chunks match "' + q.slice(0, 40) + '"';
10230
- }
10231
- } else if (noRes) {
10232
- noRes.remove();
10233
- }
10234
9963
  }
10235
9964
 
10236
9965
  async function deleteChunk(id) {
@@ -10257,28 +9986,8 @@ async function loadAgentGraphTab() {
10257
9986
  const bar = document.getElementById('ag-summary-bar');
10258
9987
  if (bar) bar.innerHTML = '<div class="loading-txt">Loading…</div>';
10259
9988
  try {
10260
- const raw = await apiFetch('/api/graph?dir=' + enc(DIR));
10261
- // API returns { nodes, edges } — normalize into the shape renderers expect
10262
- const sesNodes = (raw.nodes || []).filter(n => n.type === 'session');
10263
- const agNodes = (raw.nodes || []).filter(n => n.type === 'agenttype');
10264
- const sessions = sesNodes.map(n => ({
10265
- id: n.id,
10266
- file: n.id,
10267
- turns: n.turns || 0,
10268
- toolCount: n.totalTools || 0,
10269
- spawnCount: Object.values(n.agentSpawns || {}).reduce((a, b) => a + b, 0),
10270
- cost: n.cost || 0,
10271
- agentTypes: n.agentSpawns || {},
10272
- tools: n.toolCounts || {},
10273
- }));
10274
- _agData = {
10275
- sessions,
10276
- sessionCount: sessions.length,
10277
- agentTypes: agNodes.length,
10278
- totalSpawns: agNodes.reduce((a, n) => a + (n.totalSpawns || 0), 0),
10279
- totalToolCalls: sesNodes.reduce((a, n) => a + (n.totalTools || 0), 0),
10280
- totalCost: sesNodes.reduce((a, n) => a + (n.cost || 0), 0),
10281
- };
9989
+ const data = await apiFetch('/api/graph?dir=' + enc(DIR));
9990
+ _agData = data;
10282
9991
  _renderAgSummary();
10283
9992
  _renderAgSessList();
10284
9993
  } catch (e) {
@@ -10309,27 +10018,15 @@ function _renderAgSessList() {
10309
10018
  const el = document.getElementById('ag-sess-list');
10310
10019
  if (!_agData || !el) return;
10311
10020
  const sessions = _agData.sessions || [];
10312
- if (!sessions.length) { el.innerHTML = '<div class="empty" style="font-size:12px">No sessions<div style="font-size:11px;color:var(--text-xs);margin-top:4px">Sessions are recorded when Claude Code runs inside this project.</div></div>'; return; }
10313
- el.innerHTML =
10314
- '<input class="filter-input" id="ag-sess-filter" type="text" placeholder="Filter sessions…" style="width:100%;margin-bottom:6px;font-size:11px;box-sizing:border-box" oninput="filterAgSessions(this.value)" title="Filter by session ID">' +
10315
- '<div id="ag-sess-rows">' +
10316
- sessions.map((s, i) =>
10317
- '<div class="sess-row" style="margin-bottom:4px" data-sid="' + esc(s.id || s.file || '') + '" onclick="selectAgSession(' + i + ')">' +
10318
- '<div class="sr-top">' +
10319
- '<div class="sr-prompt" style="font-size:11px" title="' + esc(s.id || s.file || '') + '">' + esc((s.id || s.file || '').slice(-16)) + '</div>' +
10320
- '</div>' +
10321
- '<div style="font-size:10px;color:var(--text-lo);margin-top:1px">' + (s.spawnCount || 0) + ' spawns · ' + (s.toolCount || 0) + ' tools</div>' +
10322
- '</div>'
10323
- ).join('') +
10324
- '</div>';
10325
- }
10326
-
10327
- function filterAgSessions(q) {
10328
- const lq = (q || '').toLowerCase();
10329
- document.querySelectorAll('#ag-sess-rows .sess-row').forEach(el => {
10330
- const sid = (el.dataset.sid || '').toLowerCase();
10331
- el.style.display = (!lq || sid.includes(lq)) ? '' : 'none';
10332
- });
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('');
10333
10030
  }
10334
10031
 
10335
10032
  function selectAgSession(idx) {
@@ -10338,10 +10035,8 @@ function selectAgSession(idx) {
10338
10035
  document.querySelectorAll('#ag-sess-list .sess-row').forEach((el, i) => el.classList.toggle('active', i === idx));
10339
10036
  const detail = document.getElementById('ag-detail');
10340
10037
  if (!detail) return;
10341
- const agAllArr = Object.entries(s.agentTypes || {}).sort((a, b) => b[1] - a[1]);
10342
- const toolAllArr = Object.entries(s.tools || {}).sort((a, b) => b[1] - a[1]);
10343
- const agArr = agAllArr.slice(0, 12);
10344
- const toolArr = toolAllArr.slice(0, 15);
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);
10345
10040
  const maxAg = agArr.length ? Math.max(...agArr.map(x => x[1])) : 1;
10346
10041
  const maxTool = toolArr.length ? Math.max(...toolArr.map(x => x[1])) : 1;
10347
10042
  detail.innerHTML =
@@ -10351,20 +10046,20 @@ function selectAgSession(idx) {
10351
10046
  '<span>Tools: <b>' + (s.toolCount || 0) + '</b></span>' +
10352
10047
  (s.cost != null ? '<span style="color:var(--accent)">$' + Number(s.cost).toFixed(4) + '</span>' : '') +
10353
10048
  '</div>' +
10354
- (agArr.length ? '<div class="m-group-title" style="margin-bottom:5px">Agent Types' + (agAllArr.length > 12 ? '<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:6px">top 12 of ' + agAllArr.length + '</span>' : '') + '</div>' +
10049
+ (agArr.length ? '<div class="m-group-title" style="margin-bottom:5px">Agent Types</div>' +
10355
10050
  agArr.map(function(entry) {
10356
10051
  var type = entry[0], count = entry[1];
10357
10052
  return '<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">' +
10358
- '<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>' +
10359
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>' +
10360
10055
  '<div style="width:22px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">' + count + '</div>' +
10361
10056
  '</div>';
10362
10057
  }).join('') : '') +
10363
- (toolArr.length ? '<div class="m-group-title" style="margin-bottom:5px;margin-top:14px">Top Tools' + (toolAllArr.length > 15 ? '<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:6px">top 15 of ' + toolAllArr.length + '</span>' : '') + '</div>' +
10058
+ (toolArr.length ? '<div class="m-group-title" style="margin-bottom:5px;margin-top:14px">Top Tools</div>' +
10364
10059
  toolArr.map(function(entry) {
10365
10060
  var tool = entry[0], count = entry[1];
10366
10061
  return '<div style="display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px">' +
10367
- '<div style="width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-hi);font-family:var(--mono);font-size:11px" title="' + esc(tool) + '">' + esc(tool) + '</div>' +
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>' +
10368
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>' +
10369
10064
  '<div style="width:22px;text-align:right;color:var(--text-lo);font-size:11px;font-family:var(--mono)">' + count + '</div>' +
10370
10065
  '</div>';
@@ -10428,10 +10123,10 @@ function mmSwitchTab(tab) {
10428
10123
  const body = document.getElementById('mm-body');
10429
10124
  if (!body) return;
10430
10125
  if (tab === 'orgs') mmRenderOrgs(body);
10431
- else if (tab === 'skills') { mmRenderSkills(body); setTimeout(() => document.getElementById('mm-skill-filter-input')?.focus(), 50); }
10432
- 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>'); }
10433
10128
  else if (tab === 'createorg') mmRenderCreateOrg(body);
10434
- 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>'); }
10435
10130
  else if (tab === 'graph') mmRenderGraph(body);
10436
10131
  }
10437
10132
 
@@ -10442,7 +10137,7 @@ function mmRenderOrgs(body) {
10442
10137
  const running = o.running;
10443
10138
  return `<div class="mm-skill-item" onclick="closeMastermind();v2SelectOrg(${JSON.stringify(o.name)});switchView('orgs')">
10444
10139
  <span class="mm-skill-name">${esc(o.name)}</span>
10445
- <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>
10446
10141
  </div>`;
10447
10142
  }).join('');
10448
10143
  }
@@ -10451,8 +10146,8 @@ let _mmSkillFilter = '';
10451
10146
  function mmRenderSkills(body) {
10452
10147
  const q = _mmSkillFilter.toLowerCase();
10453
10148
  const filtered = q ? _MM_SKILLS_CATALOG.filter(s => s.name.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : _MM_SKILLS_CATALOG;
10454
- body.innerHTML = `<div class="filter-bar" style="margin-bottom:12px"><input class="filter-input" id="mm-skill-filter-input" type="text" placeholder="Search skills…" value="${esc(_mmSkillFilter)}" oninput="_mmSkillFilter=this.value;mmRenderSkills(document.getElementById('mm-body'))"></div>` +
10455
- filtered.map(s => `<div class="mm-skill-item" title="Click to copy: /${esc(s.name)}" onclick="navigator.clipboard.writeText(${JSON.stringify(s.name)}).then(()=>showToast('Copied',${JSON.stringify(s.name)},'ok'))">
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'))">
10456
10151
  <span class="mm-skill-name">${esc(s.name)}</span>
10457
10152
  <span class="mm-skill-desc">${esc(s.desc)}</span>
10458
10153
  </div>`).join('');
@@ -10464,66 +10159,30 @@ async function mmRenderLoops(body) {
10464
10159
  const data = await apiFetch('/api/loops?dir=' + enc(DIR));
10465
10160
  const loops = Array.isArray(data) ? data : (data.loops || []);
10466
10161
  if (!loops.length) { body.innerHTML = '<div class="empty">No active loops. Use /mastermind:autodev --tillend to start one.</div>'; return; }
10467
- const mmHasRepeat = loops.some(l => l.source === '_repeat.md');
10468
- const mmRepeatPrompts = new Set(loops.filter(l => l.source === '_repeat.md').map(l => (l.prompt || '').trim()));
10469
- const mmDeduped = loops.filter(l => {
10470
- if (l.source === 'scheduled_tasks_lock' && mmHasRepeat) return false;
10471
- if (l.source !== 'schedule_wakeup_hook') return true;
10472
- const m = (l.prompt || '').match(/--loop\s+\S+\s+(.+)$/s);
10473
- return !m || !mmRepeatPrompts.has(m[1].trim());
10474
- });
10475
- const _mmLoopsRefresh = '<div style="display:flex;justify-content:flex-end;margin-bottom:8px"><button class="btn" title="Refresh loop status" style="font-size:10px" onclick="mmRenderLoops(document.getElementById(\'mm-body\'))">↺ Refresh</button></div>';
10476
- body.innerHTML = _mmLoopsRefresh + mmDeduped.map(l => {
10477
- const isHilMm = l.status === 'hil:pending';
10478
- const isTillendMm = l.type === 'tillend';
10479
- const curRep = l.currentRep || 0;
10480
- const maxReps = l.maxReps || 0;
10481
- const nextAt = l.nextRunAt ? parseInt(l.nextRunAt) : 0;
10482
- const isExplicitlyActiveMm = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
10483
- const _STALE = 2 * 60 * 60 * 1000;
10484
- const isOverdueMm = !l.status?.startsWith('hil') && !isExplicitlyActiveMm &&
10485
- nextAt > 0 && nextAt <= Date.now();
10486
- const isStaledActiveMm = isExplicitlyActiveMm && nextAt > 0 && (Date.now() - nextAt) > _STALE;
10487
- const isFinishedMm = isOverdueMm || isStaledActiveMm ||
10488
- (!isExplicitlyActiveMm && maxReps > 0 && curRep >= maxReps) ||
10489
- l.status === 'finished' || l.status === 'done' ||
10490
- l.status === 'complete' || l.status === 'completed' || l.status === 'expired';
10491
- const running = !isFinishedMm && l.status !== 'stopped' && l.status !== 'paused';
10492
- const _mmLp = (function(_l) {
10493
- if (_l.command) return { userPrompt: _l.prompt || '', command: _l.command };
10494
- const full = _l.prompt || '';
10495
- const cmdM = full.match(/^(\/\S+)/);
10496
- if (!cmdM) return { userPrompt: full, command: '' };
10497
- const tokens = full.slice(cmdM[1].length).trim().split(/\s+/);
10498
- let ti = 0;
10499
- while (ti < tokens.length && tokens[ti] && tokens[ti].startsWith('--')) {
10500
- ti++;
10501
- if (ti < tokens.length && tokens[ti] && !tokens[ti].startsWith('--')) ti++;
10502
- }
10503
- return { userPrompt: tokens.slice(ti).join(' '), command: cmdM[1] };
10504
- })(l);
10505
- const name = (l.name || _mmLp.userPrompt || _mmLp.command || 'loop').slice(0, 60);
10506
- const ms = nextAt ? nextAt - Date.now() : 0;
10507
- const cdown = ms > 0 ? (Math.floor(ms/60000) + 'm ' + Math.floor((ms%60000)/1000) + 's') : '';
10508
- const intervalMm = fmtInterval(l.interval || l.schedule);
10509
- const runCount = isTillendMm
10510
- ? `run ${curRep} / ∞${maxReps ? ' (cap: ' + maxReps + ')' : ''}`
10511
- : (maxReps > 0 ? `run ${curRep} / ${maxReps}` : (curRep ? `run ${curRep}` : ''));
10512
- const statusLabel = isHilMm ? '⚠ HIL' : (running ? 'active' : (isFinishedMm ? 'done' : 'stopped'));
10513
- const statusColor = isHilMm ? 'oklch(75% 0.16 60)' : '';
10514
- const typeIco = isTillendMm ? '∞ ' : '↺ ';
10515
- const _sMs = l.startedAt ? (typeof l.startedAt === 'number' ? l.startedAt : new Date(l.startedAt).getTime()) : 0;
10516
- const ageMs = _sMs > 0 && _sMs < Date.now() ? Date.now() - _sMs : 0;
10517
- const ageStr = ageMs > 0 ? fmtDur(ageMs) : '';
10518
- const metaParts = [intervalMm, ageStr ? 'running ' + ageStr : '', cdown ? 'next in ' + cdown : '', runCount].filter(Boolean).join(' · ');
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 ? '∞' : '↺';
10519
10177
  return `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
10520
10178
  <div style="display:flex;align-items:center;gap:8px">
10521
- <span style="font-size:13px;color:var(--text-hi);flex:1">${typeIco}${esc(name)}</span>
10522
- <span class="ss-pill ${running && !isHilMm ? 'on' : ''}" style="${statusColor ? 'color:'+statusColor+';background:oklch(65% 0.15 60 / 0.15);border:none' : ''}">${statusLabel}</span>
10523
- ${running ? `<button class="btn" title="Stop this automation loop" style="font-size:10px;color:var(--red);border-color:var(--red)" onclick="stopLoop(event,${JSON.stringify(l.id||l.name||'')});mmSwitchTab('loops')">■ Stop</button>` : ''}
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>` : ''}
10524
10184
  </div>
10525
- <div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(metaParts)}</div>
10526
- ${isHilMm ? `<div style="font-size:10px;color:oklch(75% 0.16 60);margin-top:4px">⚠ Waiting for human response — check HIL file to resume</div>` : ''}
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>
10527
10186
  </div>`;
10528
10187
  }).join('');
10529
10188
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
@@ -10537,17 +10196,17 @@ function mmRenderCreateOrg(body) {
10537
10196
  <div><div class="le-lbl">Org Name</div><input id="mco-name" class="filter-input" placeholder="my-team"></div>
10538
10197
  <div><div class="le-lbl">Goal</div><input id="mco-goal" class="filter-input" placeholder="Build and ship features autonomously"></div>
10539
10198
  <div><div class="le-lbl">Topology</div>
10540
- <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">
10541
10200
  ${topos.map(t => `<option>${t}</option>`).join('')}
10542
10201
  </select>
10543
10202
  </div>
10544
10203
  <div><div class="le-lbl">Adapter</div>
10545
- <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">
10546
10205
  ${adapters.map(a => `<option>${a}</option>`).join('')}
10547
10206
  </select>
10548
10207
  </div>
10549
- <div><div class="le-lbl">Max Agents</div><input id="mco-agents" class="filter-input" type="number" value="8" min="1" max="50" title="Maximum concurrent agents (1–50)"></div>
10550
- <button class="btn" title="Generate a monomind CLI command to create this org" style="width:fit-content;color:oklch(65% 0.16 295);border-color:oklch(65% 0.16 295)" onclick="mmGenerateOrgCmd()">Generate CLI Command</button>
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>
10551
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>
10552
10211
  </div>`;
10553
10212
  }
@@ -10591,7 +10250,6 @@ async function mmRenderMetrics(body) {
10591
10250
  </div>
10592
10251
  <div class="m-group-title" style="margin-bottom:8px">Swarm</div>
10593
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>
10594
- <div style="margin-top:12px;display:flex;justify-content:flex-end"><button class="btn" title="Refresh metrics" style="font-size:10px" onclick="mmRenderMetrics(document.getElementById('mm-body'))">↺ Refresh</button></div>
10595
10253
  `;
10596
10254
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
10597
10255
  }
@@ -10617,13 +10275,13 @@ async function mmRenderGraph(body) {
10617
10275
  <div class="chunk-stat"><div class="chunk-stat-val">${Object.keys(nodeTypes).length}</div><div class="chunk-stat-lbl">Types</div></div>
10618
10276
  </div>
10619
10277
  ${topNodes.length ? `<div class="m-group-title" style="margin-bottom:6px">God Nodes</div>
10620
- ${topNodes.map(n => `<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border);cursor:pointer" data-nid="${esc(n.id || n.name || '')}" title="Open '${esc(n.name || n.id || '')}' in Monograph wiki (degree: ${n.degree ?? '—'})" onclick="var _nid=this.dataset.nid;closeMastermind();switchView('monograph');setTimeout(function(){mgWikiJumpToNode(_nid)},300)" onmouseenter="this.style.background='var(--hover)'" onmouseleave="this.style.background=''">
10278
+ ${topNodes.map(n => `<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)">
10621
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>
10622
- <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>
10623
10281
  </div>`).join('')}` : ''}
10624
10282
  <div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
10625
- <button class="btn" title="Switch to the Monograph view" onclick="switchView('monograph');closeMastermind()">Open Monograph →</button>
10626
- <button class="btn" title="Trigger a full knowledge graph rebuild" onclick="fetch('/api/monograph-build?dir='+enc(DIR),{method:'POST'}).then(()=>showToast('Building','Graph rebuild started','ok'))">↺ Rebuild Graph</button>
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>
10627
10285
  </div>
10628
10286
  `;
10629
10287
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }