@monoes/monomindcli 1.14.7 → 1.15.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 (329) hide show
  1. package/.claude/agents/reengineer-squad/boss.md +113 -0
  2. package/.claude/agents/reengineer-squad/critic-architect.md +132 -0
  3. package/.claude/agents/reengineer-squad/git-manager.md +145 -0
  4. package/.claude/agents/reengineer-squad/idea-generator.md +95 -0
  5. package/.claude/agents/reengineer-squad/implementer.md +112 -0
  6. package/.claude/agents/reengineer-squad/integration-planner.md +112 -0
  7. package/.claude/agents/reengineer-squad/source-analyst.md +103 -0
  8. package/.claude/agents/reengineer-squad/target-analyst.md +118 -0
  9. package/.claude/agents/reengineer-squad/tester.md +105 -0
  10. package/.claude/commands/mastermind/master.md +35 -14
  11. package/.claude/helpers/handlers/capture-handler.cjs +155 -18
  12. package/.claude/helpers/monolean-activate.cjs +20 -0
  13. package/.claude/helpers/monolean-config.cjs +76 -0
  14. package/.claude/helpers/monolean-instructions.cjs +109 -0
  15. package/.claude/helpers/monolean-propagate.cjs +9 -0
  16. package/.claude/helpers/monolean-tracker.cjs +18 -0
  17. package/.claude/helpers/skill-registry.json +2 -2
  18. package/.claude/skills/agent-browser-testing/SKILL.md +301 -18
  19. package/.claude/skills/mastermind/runorg.md +69 -23
  20. package/.claude/skills/monodesign/SKILL.md +32 -1
  21. package/.claude/skills/monodesign/adapt.md +53 -0
  22. package/.claude/skills/monodesign/agents/monodesign-asset-producer.md +100 -0
  23. package/.claude/skills/monodesign/animate.md +65 -0
  24. package/.claude/skills/monodesign/audit.md +89 -0
  25. package/.claude/skills/monodesign/bolder.md +50 -0
  26. package/.claude/skills/monodesign/clarify.md +64 -0
  27. package/.claude/skills/monodesign/colorize.md +68 -0
  28. package/.claude/skills/monodesign/craft.md +51 -0
  29. package/.claude/skills/monodesign/critique.md +66 -0
  30. package/.claude/skills/monodesign/delight.md +47 -0
  31. package/.claude/skills/monodesign/distill.md +56 -0
  32. package/.claude/skills/monodesign/document.md +80 -0
  33. package/.claude/skills/monodesign/extract.md +74 -0
  34. package/.claude/skills/monodesign/harden.md +65 -0
  35. package/.claude/skills/monodesign/live.md +59 -0
  36. package/.claude/skills/monodesign/onboard.md +50 -0
  37. package/.claude/skills/monodesign/optimize.md +64 -0
  38. package/.claude/skills/monodesign/overdrive.md +56 -0
  39. package/.claude/skills/monodesign/polish.md +68 -0
  40. package/.claude/skills/monodesign/quieter.md +57 -0
  41. package/.claude/skills/monodesign/reference/antipatterns-catalog.md +248 -76
  42. package/.claude/skills/monodesign/reference/codex.md +107 -0
  43. package/.claude/skills/monodesign/reference/craft.md +3 -0
  44. package/.claude/skills/monodesign/reference/hooks.md +99 -0
  45. package/.claude/skills/monodesign/reference/image-prompts.md +12 -0
  46. package/.claude/skills/monodesign/shape.md +71 -0
  47. package/.claude/skills/monodesign/teach.md +69 -0
  48. package/.claude/skills/monodesign/typeset.md +59 -0
  49. package/.claude/skills/monolean/SKILL.md +118 -0
  50. package/.claude/skills/monolean-audit/SKILL.md +41 -0
  51. package/.claude/skills/monolean-debt/SKILL.md +46 -0
  52. package/.claude/skills/monolean-help/SKILL.md +60 -0
  53. package/.claude/skills/monolean-review/SKILL.md +57 -0
  54. package/bin/cli.js +3 -1
  55. package/dist/dashboard/server.js +137 -0
  56. package/dist/src/__tests__/browse-adapters.test.d.ts +2 -0
  57. package/dist/src/__tests__/browse-adapters.test.d.ts.map +1 -0
  58. package/dist/src/__tests__/browse-adapters.test.js +51 -0
  59. package/dist/src/__tests__/browse-adapters.test.js.map +1 -0
  60. package/dist/src/__tests__/browse-analyzer.test.d.ts +2 -0
  61. package/dist/src/__tests__/browse-analyzer.test.d.ts.map +1 -0
  62. package/dist/src/__tests__/browse-analyzer.test.js +68 -0
  63. package/dist/src/__tests__/browse-analyzer.test.js.map +1 -0
  64. package/dist/src/__tests__/browse-builtin-handlers.test.d.ts +2 -0
  65. package/dist/src/__tests__/browse-builtin-handlers.test.d.ts.map +1 -0
  66. package/dist/src/__tests__/browse-builtin-handlers.test.js +139 -0
  67. package/dist/src/__tests__/browse-builtin-handlers.test.js.map +1 -0
  68. package/dist/src/__tests__/browse-cdp.test.d.ts +2 -0
  69. package/dist/src/__tests__/browse-cdp.test.d.ts.map +1 -0
  70. package/dist/src/__tests__/browse-cdp.test.js +169 -0
  71. package/dist/src/__tests__/browse-cdp.test.js.map +1 -0
  72. package/dist/src/__tests__/browse-dashboard.test.d.ts +2 -0
  73. package/dist/src/__tests__/browse-dashboard.test.d.ts.map +1 -0
  74. package/dist/src/__tests__/browse-dashboard.test.js +179 -0
  75. package/dist/src/__tests__/browse-dashboard.test.js.map +1 -0
  76. package/dist/src/__tests__/browse-engine.test.d.ts +2 -0
  77. package/dist/src/__tests__/browse-engine.test.d.ts.map +1 -0
  78. package/dist/src/__tests__/browse-engine.test.js +122 -0
  79. package/dist/src/__tests__/browse-engine.test.js.map +1 -0
  80. package/dist/src/__tests__/browse-expression.test.d.ts +2 -0
  81. package/dist/src/__tests__/browse-expression.test.d.ts.map +1 -0
  82. package/dist/src/__tests__/browse-expression.test.js +54 -0
  83. package/dist/src/__tests__/browse-expression.test.js.map +1 -0
  84. package/dist/src/__tests__/browse-store.test.d.ts +2 -0
  85. package/dist/src/__tests__/browse-store.test.d.ts.map +1 -0
  86. package/dist/src/__tests__/browse-store.test.js +99 -0
  87. package/dist/src/__tests__/browse-store.test.js.map +1 -0
  88. package/dist/src/__tests__/browse-workflow-types.test.d.ts +2 -0
  89. package/dist/src/__tests__/browse-workflow-types.test.d.ts.map +1 -0
  90. package/dist/src/__tests__/browse-workflow-types.test.js +33 -0
  91. package/dist/src/__tests__/browse-workflow-types.test.js.map +1 -0
  92. package/dist/src/browser/action-builder/analyzer.d.ts +11 -0
  93. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
  94. package/dist/src/browser/action-builder/analyzer.js +71 -0
  95. package/dist/src/browser/action-builder/analyzer.js.map +1 -0
  96. package/dist/src/browser/action-builder/types.d.ts +47 -0
  97. package/dist/src/browser/action-builder/types.d.ts.map +1 -0
  98. package/dist/src/browser/action-builder/types.js +2 -0
  99. package/dist/src/browser/action-builder/types.js.map +1 -0
  100. package/dist/src/browser/adapters/gemini.d.ts +3 -0
  101. package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
  102. package/dist/src/browser/adapters/gemini.js +16 -0
  103. package/dist/src/browser/adapters/gemini.js.map +1 -0
  104. package/dist/src/browser/adapters/google.d.ts +3 -0
  105. package/dist/src/browser/adapters/google.d.ts.map +1 -0
  106. package/dist/src/browser/adapters/google.js +17 -0
  107. package/dist/src/browser/adapters/google.js.map +1 -0
  108. package/dist/src/browser/adapters/index.d.ts +19 -0
  109. package/dist/src/browser/adapters/index.d.ts.map +1 -0
  110. package/dist/src/browser/adapters/index.js +23 -0
  111. package/dist/src/browser/adapters/index.js.map +1 -0
  112. package/dist/src/browser/adapters/instagram.d.ts +3 -0
  113. package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
  114. package/dist/src/browser/adapters/instagram.js +17 -0
  115. package/dist/src/browser/adapters/instagram.js.map +1 -0
  116. package/dist/src/browser/adapters/linkedin.d.ts +3 -0
  117. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
  118. package/dist/src/browser/adapters/linkedin.js +19 -0
  119. package/dist/src/browser/adapters/linkedin.js.map +1 -0
  120. package/dist/src/browser/adapters/microsoft.d.ts +3 -0
  121. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
  122. package/dist/src/browser/adapters/microsoft.js +16 -0
  123. package/dist/src/browser/adapters/microsoft.js.map +1 -0
  124. package/dist/src/browser/adapters/x.d.ts +3 -0
  125. package/dist/src/browser/adapters/x.d.ts.map +1 -0
  126. package/dist/src/browser/adapters/x.js +19 -0
  127. package/dist/src/browser/adapters/x.js.map +1 -0
  128. package/dist/src/browser/dashboard/api-types.d.ts +50 -0
  129. package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
  130. package/dist/src/browser/dashboard/api-types.js +14 -0
  131. package/dist/src/browser/dashboard/api-types.js.map +1 -0
  132. package/dist/src/browser/dashboard/server.d.ts +9 -0
  133. package/dist/src/browser/dashboard/server.d.ts.map +1 -0
  134. package/dist/src/browser/dashboard/server.js +62 -0
  135. package/dist/src/browser/dashboard/server.js.map +1 -0
  136. package/dist/src/browser/dashboard/ui.html +1811 -0
  137. package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
  138. package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
  139. package/dist/src/browser/workflow/builtin-handlers.js +343 -0
  140. package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
  141. package/dist/src/browser/workflow/engine.d.ts +15 -0
  142. package/dist/src/browser/workflow/engine.d.ts.map +1 -0
  143. package/dist/src/browser/workflow/engine.js +127 -0
  144. package/dist/src/browser/workflow/engine.js.map +1 -0
  145. package/dist/src/browser/workflow/expression.d.ts +4 -0
  146. package/dist/src/browser/workflow/expression.d.ts.map +1 -0
  147. package/dist/src/browser/workflow/expression.js +64 -0
  148. package/dist/src/browser/workflow/expression.js.map +1 -0
  149. package/dist/src/browser/workflow/store.d.ts +24 -0
  150. package/dist/src/browser/workflow/store.d.ts.map +1 -0
  151. package/dist/src/browser/workflow/store.js +145 -0
  152. package/dist/src/browser/workflow/store.js.map +1 -0
  153. package/dist/src/browser/workflow/types.d.ts +48 -0
  154. package/dist/src/browser/workflow/types.d.ts.map +1 -0
  155. package/dist/src/browser/workflow/types.js +2 -0
  156. package/dist/src/browser/workflow/types.js.map +1 -0
  157. package/dist/src/commands/browse-action.d.ts +4 -0
  158. package/dist/src/commands/browse-action.d.ts.map +1 -0
  159. package/dist/src/commands/browse-action.js +151 -0
  160. package/dist/src/commands/browse-action.js.map +1 -0
  161. package/dist/src/commands/browse-platform.d.ts +4 -0
  162. package/dist/src/commands/browse-platform.d.ts.map +1 -0
  163. package/dist/src/commands/browse-platform.js +117 -0
  164. package/dist/src/commands/browse-platform.js.map +1 -0
  165. package/dist/src/commands/browse-workflow.d.ts +4 -0
  166. package/dist/src/commands/browse-workflow.d.ts.map +1 -0
  167. package/dist/src/commands/browse-workflow.js +153 -0
  168. package/dist/src/commands/browse-workflow.js.map +1 -0
  169. package/dist/src/commands/browse.d.ts +10 -6
  170. package/dist/src/commands/browse.d.ts.map +1 -1
  171. package/dist/src/commands/browse.js +11 -2154
  172. package/dist/src/commands/browse.js.map +1 -1
  173. package/dist/src/commands/design-detect.d.ts +21 -0
  174. package/dist/src/commands/design-detect.d.ts.map +1 -0
  175. package/dist/src/commands/design-detect.js +127 -0
  176. package/dist/src/commands/design-detect.js.map +1 -0
  177. package/dist/src/commands/design-palette.d.ts +22 -0
  178. package/dist/src/commands/design-palette.d.ts.map +1 -0
  179. package/dist/src/commands/design-palette.js +539 -0
  180. package/dist/src/commands/design-palette.js.map +1 -0
  181. package/dist/src/commands/hooks-core-commands.d.ts +10 -0
  182. package/dist/src/commands/hooks-core-commands.d.ts.map +1 -0
  183. package/dist/src/commands/hooks-core-commands.js +377 -0
  184. package/dist/src/commands/hooks-core-commands.js.map +1 -0
  185. package/dist/src/commands/hooks-coverage-commands.d.ts +12 -0
  186. package/dist/src/commands/hooks-coverage-commands.d.ts.map +1 -0
  187. package/dist/src/commands/hooks-coverage-commands.js +1217 -0
  188. package/dist/src/commands/hooks-coverage-commands.js.map +1 -0
  189. package/dist/src/commands/hooks-coverage-utils.d.ts +42 -0
  190. package/dist/src/commands/hooks-coverage-utils.d.ts.map +1 -0
  191. package/dist/src/commands/hooks-coverage-utils.js +220 -0
  192. package/dist/src/commands/hooks-coverage-utils.js.map +1 -0
  193. package/dist/src/commands/hooks-extended-commands.d.ts +14 -0
  194. package/dist/src/commands/hooks-extended-commands.d.ts.map +1 -0
  195. package/dist/src/commands/hooks-extended-commands.js +579 -0
  196. package/dist/src/commands/hooks-extended-commands.js.map +1 -0
  197. package/dist/src/commands/hooks-formatting.d.ts +13 -0
  198. package/dist/src/commands/hooks-formatting.d.ts.map +1 -0
  199. package/dist/src/commands/hooks-formatting.js +42 -0
  200. package/dist/src/commands/hooks-formatting.js.map +1 -0
  201. package/dist/src/commands/hooks-routing-commands.d.ts +15 -0
  202. package/dist/src/commands/hooks-routing-commands.d.ts.map +1 -0
  203. package/dist/src/commands/hooks-routing-commands.js +723 -0
  204. package/dist/src/commands/hooks-routing-commands.js.map +1 -0
  205. package/dist/src/commands/hooks-workers.d.ts +9 -0
  206. package/dist/src/commands/hooks-workers.d.ts.map +1 -0
  207. package/dist/src/commands/hooks-workers.js +782 -0
  208. package/dist/src/commands/hooks-workers.js.map +1 -0
  209. package/dist/src/commands/hooks.d.ts +8 -0
  210. package/dist/src/commands/hooks.d.ts.map +1 -1
  211. package/dist/src/commands/hooks.js +179 -4103
  212. package/dist/src/commands/hooks.js.map +1 -1
  213. package/dist/src/commands/index.d.ts +1 -0
  214. package/dist/src/commands/index.d.ts.map +1 -1
  215. package/dist/src/commands/index.js +6 -0
  216. package/dist/src/commands/index.js.map +1 -1
  217. package/dist/src/commands/org.d.ts.map +1 -1
  218. package/dist/src/commands/org.js +14 -15
  219. package/dist/src/commands/org.js.map +1 -1
  220. package/dist/src/commands/tokens.d.ts.map +1 -1
  221. package/dist/src/commands/tokens.js +77 -1
  222. package/dist/src/commands/tokens.js.map +1 -1
  223. package/dist/src/init/executor.d.ts.map +1 -1
  224. package/dist/src/init/executor.js +18 -8
  225. package/dist/src/init/executor.js.map +1 -1
  226. package/dist/src/init/settings-generator.d.ts.map +1 -1
  227. package/dist/src/init/settings-generator.js +39 -5
  228. package/dist/src/init/settings-generator.js.map +1 -1
  229. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  230. package/dist/src/init/statusline-generator.js +25 -5
  231. package/dist/src/init/statusline-generator.js.map +1 -1
  232. package/dist/src/mcp-tools/browser-tools.d.ts +3 -5
  233. package/dist/src/mcp-tools/browser-tools.d.ts.map +1 -1
  234. package/dist/src/mcp-tools/browser-tools.js +619 -326
  235. package/dist/src/mcp-tools/browser-tools.js.map +1 -1
  236. package/dist/src/mcp-tools/hooks-embedding.d.ts +161 -0
  237. package/dist/src/mcp-tools/hooks-embedding.d.ts.map +1 -0
  238. package/dist/src/mcp-tools/hooks-embedding.js +506 -0
  239. package/dist/src/mcp-tools/hooks-embedding.js.map +1 -0
  240. package/dist/src/mcp-tools/hooks-intelligence.d.ts +26 -0
  241. package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -0
  242. package/dist/src/mcp-tools/hooks-intelligence.js +1328 -0
  243. package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -0
  244. package/dist/src/mcp-tools/hooks-routing.d.ts +27 -0
  245. package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -0
  246. package/dist/src/mcp-tools/hooks-routing.js +1591 -0
  247. package/dist/src/mcp-tools/hooks-routing.js.map +1 -0
  248. package/dist/src/mcp-tools/hooks-tools.d.ts +3 -38
  249. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  250. package/dist/src/mcp-tools/hooks-tools.js +5 -3393
  251. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  252. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  253. package/dist/src/mcp-tools/monograph-tools.js +24 -14
  254. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  255. package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -1
  256. package/dist/src/mcp-tools/workflow-tools.js +54 -1
  257. package/dist/src/mcp-tools/workflow-tools.js.map +1 -1
  258. package/dist/src/memory/embedding-operations.d.ts +58 -0
  259. package/dist/src/memory/embedding-operations.d.ts.map +1 -0
  260. package/dist/src/memory/embedding-operations.js +299 -0
  261. package/dist/src/memory/embedding-operations.js.map +1 -0
  262. package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
  263. package/dist/src/memory/ewc-consolidation.js +37 -3
  264. package/dist/src/memory/ewc-consolidation.js.map +1 -1
  265. package/dist/src/memory/hnsw-operations.d.ts +130 -0
  266. package/dist/src/memory/hnsw-operations.d.ts.map +1 -0
  267. package/dist/src/memory/hnsw-operations.js +400 -0
  268. package/dist/src/memory/hnsw-operations.js.map +1 -0
  269. package/dist/src/memory/intelligence.d.ts.map +1 -1
  270. package/dist/src/memory/intelligence.js +42 -23
  271. package/dist/src/memory/intelligence.js.map +1 -1
  272. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  273. package/dist/src/memory/memory-bridge.js +52 -8
  274. package/dist/src/memory/memory-bridge.js.map +1 -1
  275. package/dist/src/memory/memory-crud.d.ts +67 -0
  276. package/dist/src/memory/memory-crud.d.ts.map +1 -0
  277. package/dist/src/memory/memory-crud.js +415 -0
  278. package/dist/src/memory/memory-crud.js.map +1 -0
  279. package/dist/src/memory/memory-initializer.d.ts +9 -322
  280. package/dist/src/memory/memory-initializer.d.ts.map +1 -1
  281. package/dist/src/memory/memory-initializer.js +17 -1794
  282. package/dist/src/memory/memory-initializer.js.map +1 -1
  283. package/dist/src/memory/memory-migrations.d.ts +30 -0
  284. package/dist/src/memory/memory-migrations.d.ts.map +1 -0
  285. package/dist/src/memory/memory-migrations.js +134 -0
  286. package/dist/src/memory/memory-migrations.js.map +1 -0
  287. package/dist/src/memory/memory-read.d.ts +78 -0
  288. package/dist/src/memory/memory-read.d.ts.map +1 -0
  289. package/dist/src/memory/memory-read.js +331 -0
  290. package/dist/src/memory/memory-read.js.map +1 -0
  291. package/dist/src/memory/memory-schema.d.ts +13 -0
  292. package/dist/src/memory/memory-schema.d.ts.map +1 -0
  293. package/dist/src/memory/memory-schema.js +167 -0
  294. package/dist/src/memory/memory-schema.js.map +1 -0
  295. package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
  296. package/dist/src/memory/sona-optimizer.js +37 -4
  297. package/dist/src/memory/sona-optimizer.js.map +1 -1
  298. package/dist/src/monovector/route-outcomes.d.ts.map +1 -1
  299. package/dist/src/monovector/route-outcomes.js +16 -6
  300. package/dist/src/monovector/route-outcomes.js.map +1 -1
  301. package/dist/src/pricing/model-pricing.d.ts +41 -0
  302. package/dist/src/pricing/model-pricing.d.ts.map +1 -0
  303. package/dist/src/pricing/model-pricing.js +61 -0
  304. package/dist/src/pricing/model-pricing.js.map +1 -0
  305. package/dist/src/ui/.monomind/capture/active-run.json +1 -0
  306. package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/real-events-1782290897.convs.jsonl +3 -0
  307. package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/real-events-1782290897.jsonl +11 -0
  308. package/dist/src/ui/.monomind/orgs/system-trial-qa/runs/rigid-qa-restart-1782288201.jsonl +540 -0
  309. package/dist/src/ui/.monomind/orgs/system-trial-qa-threads.jsonl +3 -0
  310. package/dist/src/ui/.monomind/orgs/test-event-fix/runs/rigid-qa-restart-1782288201.jsonl +2 -0
  311. package/dist/src/ui/MODULARIZATION_PLAN.md +79 -0
  312. package/dist/src/ui/collector.mjs +23 -13
  313. package/dist/src/ui/dashboard.html +1652 -13
  314. package/dist/src/ui/data/known-projects.json +1 -0
  315. package/dist/src/ui/data/mastermind-events.jsonl +553 -0
  316. package/dist/src/ui/data/sessions/_index.json +1 -0
  317. package/dist/src/ui/data/sessions/final-sess-001.jsonl +542 -0
  318. package/dist/src/ui/data/unknown-events.jsonl +1 -0
  319. package/dist/src/ui/orgs.html +154 -10
  320. package/dist/src/ui/server.mjs +1131 -168
  321. package/dist/src/ui/sse-manager.mjs +119 -0
  322. package/dist/src/update/checker.js +1 -1
  323. package/dist/src/update/checker.js.map +1 -1
  324. package/dist/tsconfig.tsbuildinfo +1 -1
  325. package/dist/workflow/builtin-handlers.js +321 -0
  326. package/dist/workflow/engine.js +253 -0
  327. package/dist/workflow/expression.js +98 -0
  328. package/dist/workflow/types.js +2 -0
  329. package/package.json +8 -5
@@ -286,7 +286,9 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
286
286
  .oi-goal { font-size: 11px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
287
287
  .oi-chips { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
288
288
  .oi-chip { font-size: 10px; padding: 1px 6px; border-radius: 8px; background: var(--surface-hi); color: var(--text-lo); }
289
- .oi-chip.live { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
289
+ .oi-chip.live { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
290
+ .oi-chip.quiet { background: oklch(70% 0.12 90 / 0.12); color: oklch(78% 0.15 90); }
291
+ .oi-chip.stale { background: oklch(60% 0.15 25 / 0.12); color: oklch(70% 0.15 25); }
290
292
 
291
293
  /* detail pane */
292
294
  #org-detail-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; position: relative; }
@@ -297,8 +299,10 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
297
299
  #org-detail-head { padding: 12px 18px 11px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
298
300
  .odh-name { font-size: 16px; font-weight: 600; color: var(--text-hi); }
299
301
  .odh-badge { font-size: 11px; padding: 2px 8px; border-radius: 8px; }
300
- .odh-badge.idle { background: var(--surface-hi); color: var(--text-lo); }
301
- .odh-badge.live { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
302
+ .odh-badge.idle { background: var(--surface-hi); color: var(--text-lo); }
303
+ .odh-badge.live { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
304
+ .odh-badge.quiet { background: oklch(70% 0.12 90 / 0.12); color: oklch(78% 0.15 90); }
305
+ .odh-badge.stale { background: oklch(60% 0.15 25 / 0.12); color: oklch(70% 0.15 25); }
302
306
  .odh-pill { font-size: 11px; padding: 2px 8px; border-radius: 8px; background: var(--surface); color: var(--text-lo); border: 1px solid var(--border); }
303
307
  .odh-right { margin-left: auto; display: flex; gap: 6px; }
304
308
 
@@ -336,6 +340,15 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
336
340
  #odt-chat-feed::-webkit-scrollbar { width:4px; }
337
341
  #odt-chat-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
338
342
  #odt-chat-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
343
+ .odt-mode-btn {
344
+ font-family: inherit; font-size: 7px; letter-spacing: 1.5px;
345
+ padding: 2px 7px; border-radius: 3px; cursor: pointer;
346
+ border: 1px solid oklch(62% 0.2 186 / 0.25);
347
+ color: oklch(45% 0.07 186); background: transparent;
348
+ transition: all 0.12s;
349
+ }
350
+ .odt-mode-btn:hover { color: oklch(65% 0.12 186); border-color: oklch(62% 0.2 186 / 0.5); }
351
+ .odt-mode-btn.active { color: oklch(72% 0.18 186); border-color: oklch(62% 0.2 186 / 0.7); background: oklch(62% 0.2 186 / 0.12); }
339
352
  .cv-excerpt-banner { flex-shrink:0; padding:7px 16px 8px; background:oklch(13% 0.01 295); border-bottom:1px solid var(--border); display:none; }
340
353
  .cv-excerpt-banner.visible { display:block; }
341
354
  .cv-excerpt-label { font-size:8px; letter-spacing:2px; color:var(--text-xs); text-transform:uppercase; margin-bottom:4px; }
@@ -1339,6 +1352,150 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1339
1352
  #mem-modal-ta:focus { border-color: var(--accent); }
1340
1353
  .mem-modal-btns { display: flex; justify-content: flex-end; gap: 8px; }
1341
1354
 
1355
+ /* ── playbook view tabs ────────────────────────────────────── */
1356
+ .wf-pane { display: none; }
1357
+ .wf-pane.active { display: block; }
1358
+
1359
+ /* ── playbook builder ──────────────────────────────────────── */
1360
+ #wf-tab-builder { display: none; height: calc(100vh - 180px); min-height: 400px; }
1361
+ #wf-tab-builder.active { display: flex; flex-direction: column; }
1362
+ #pb-toolbar { display: flex; gap: 8px; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); flex-shrink: 0; }
1363
+ #pb-toolbar .btn { font-size: 11px; padding: 4px 10px; }
1364
+ #pb-name-input { background: var(--surface-hi); border: 1px solid var(--border); border-radius: 5px; color: var(--text-hi); font-size: 12px; padding: 4px 8px; flex: 1; max-width: 240px; outline: none; }
1365
+ #pb-name-input:focus { border-color: var(--accent); }
1366
+ #pb-run-status { font-size: 11px; color: var(--text-lo); margin-left: 4px; }
1367
+ #pb-editor { display: flex; flex: 1; gap: 0; overflow: hidden; }
1368
+ #pb-palette { width: 180px; flex-shrink: 0; border-right: 1px solid var(--border); overflow-y: auto; padding: 8px 0; }
1369
+ .pb-cat-hdr { font-size: 10px; font-weight: 700; color: var(--text-xs); letter-spacing: 0.06em; text-transform: uppercase; padding: 6px 12px 3px; cursor: pointer; display: flex; align-items: center; gap: 6px; }
1370
+ .pb-cat-hdr:hover { color: var(--text-lo); }
1371
+ .pb-cat-arrow { font-size: 8px; transition: transform 0.15s; }
1372
+ .pb-cat-body { overflow: hidden; }
1373
+ .pb-cat-body.collapsed { display: none; }
1374
+ .pb-node-btn { width: 100%; text-align: left; background: none; border: none; font-size: 11px; color: var(--text-lo); padding: 4px 12px 4px 20px; cursor: pointer; display: flex; align-items: center; gap: 6px; }
1375
+ .pb-node-btn:hover { background: var(--surface-hi); color: var(--text-hi); }
1376
+ .pb-node-btn .pb-node-icon { font-size: 13px; width: 16px; text-align: center; flex-shrink: 0; }
1377
+ #pb-canvas-wrap { flex: 1; position: relative; overflow: hidden; background: var(--bg); }
1378
+ #pb-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
1379
+ #pb-svg { position: absolute; top: 0; left: 0; pointer-events: none; }
1380
+ #pb-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-xs); font-size: 12px; gap: 8px; pointer-events: none; }
1381
+ .pb-node-card { position: absolute; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; width: 160px; min-height: 56px; cursor: grab; user-select: none; transition: box-shadow 0.12s, border-color 0.12s; }
1382
+ .pb-node-card:active { cursor: grabbing; }
1383
+ .pb-node-card.selected { border-color: var(--accent); box-shadow: 0 0 0 2px oklch(72% 0.18 75 / 0.25); }
1384
+ .pb-node-card.ns-running { border-color: var(--accent); }
1385
+ .pb-node-card.ns-completed { border-color: var(--green); }
1386
+ .pb-node-card.ns-failed { border-color: var(--red); }
1387
+ .pb-node-hdr { display: flex; align-items: center; gap: 6px; padding: 8px 10px 6px; border-bottom: 1px solid var(--border); }
1388
+ .pb-node-type-icon { font-size: 14px; }
1389
+ .pb-node-title { font-size: 11px; font-weight: 600; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1390
+ .pb-node-del { font-size: 12px; color: var(--text-xs); cursor: pointer; padding: 0 2px; line-height: 1; background: none; border: none; }
1391
+ .pb-node-del:hover { color: var(--red); }
1392
+ .pb-node-body { padding: 6px 10px 8px; font-size: 10px; color: var(--text-lo); line-height: 1.5; }
1393
+ .pb-node-status { display: flex; align-items: center; gap: 4px; margin-bottom: 2px; }
1394
+ .pb-ns-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border); flex-shrink: 0; }
1395
+ .pb-ns-dot.running { background: var(--accent); animation: pb-pulse 1s infinite; }
1396
+ .pb-ns-dot.completed{ background: var(--green); }
1397
+ .pb-ns-dot.failed { background: var(--red); }
1398
+ .pb-node-conn-btn { position: absolute; right: -10px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; border-radius: 50%; background: var(--accent-dim); border: 2px solid var(--accent); cursor: pointer; z-index: 10; display: flex; align-items: center; justify-content: center; font-size: 8px; color: var(--accent); }
1399
+ .pb-node-conn-btn:hover { background: var(--accent); color: #fff; }
1400
+ #pb-config { width: 220px; flex-shrink: 0; border-left: 1px solid var(--border); overflow-y: auto; padding: 12px; }
1401
+ #pb-config-title { font-size: 11px; font-weight: 700; color: var(--text-hi); margin-bottom: 10px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
1402
+ .pb-cfg-row { margin-bottom: 10px; }
1403
+ .pb-cfg-lbl { font-size: 10px; color: var(--text-lo); margin-bottom: 3px; font-weight: 600; }
1404
+ .pb-cfg-inp { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-hi); font-size: 11px; padding: 4px 7px; outline: none; resize: vertical; font-family: var(--mono); }
1405
+ .pb-cfg-inp:focus { border-color: var(--accent); }
1406
+ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1407
+ @keyframes pb-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
1408
+
1409
+ /* ── systems grid ──────────────────────────────────────────── */
1410
+ #wf-systems-grid { display: block; }
1411
+ /* ── monoagent connections tile grid ─────────────────────────── */
1412
+ .ma-cat-hdr { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
1413
+ .ma-cat-label { font-family:var(--mono); font-size:10px; font-weight:700; color:var(--text-lo); text-transform:uppercase; letter-spacing:2px; white-space:nowrap; }
1414
+ .ma-cat-line { flex:1; height:1px; background:var(--border); }
1415
+ .ma-cat-count { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); }
1416
+ .ma-tiles { display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:8px; margin-bottom:24px; }
1417
+ .ma-tile { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:16px 8px 12px; cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:6px; transition:border-color 0.15s,box-shadow 0.15s; user-select:none; }
1418
+ .ma-tile:hover { border-color:var(--border-hi); }
1419
+ .ma-tile.connected { border-color:oklch(65% 0.15 150 / 0.5); box-shadow:0 0 10px oklch(65% 0.15 150 / 0.12); }
1420
+ .ma-tile-icon { font-size:22px; line-height:1; }
1421
+ .ma-tile-name { font-family:var(--mono); font-size:10px; font-weight:600; color:var(--text-mid); text-align:center; line-height:1.3; }
1422
+ .ma-tile-dot { width:6px; height:6px; border-radius:50%; background:var(--border); }
1423
+ .ma-tile.connected .ma-tile-dot { background:var(--green); box-shadow:0 0 5px oklch(65% 0.15 150 / 0.7); }
1424
+ .ma-tile.expired .ma-tile-dot { background:#fbbf24; box-shadow:0 0 5px rgba(251,191,36,0.5); }
1425
+ .ma-tile-acct { font-family:var(--mono); font-size:9px; color:var(--text-xs); max-width:84px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1426
+ /* modal */
1427
+ .ma-modal-bg { position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:1200; display:flex; align-items:center; justify-content:center; padding:24px; }
1428
+ .ma-modal { width:100%; max-width:440px; background:var(--surface); border:1px solid var(--border-hi); border-radius:14px; box-shadow:0 8px 40px rgba(0,0,0,0.5); overflow:hidden; }
1429
+ .ma-modal-hdr { display:flex; align-items:center; justify-content:space-between; padding:16px 20px 12px; border-bottom:1px solid var(--border); }
1430
+ .ma-modal-hdr-left { display:flex; align-items:center; gap:10px; }
1431
+ .ma-modal-title { font-family:var(--mono); font-size:13px; font-weight:700; color:var(--text-hi); }
1432
+ .ma-modal-status { font-family:var(--mono); font-size:10px; text-transform:uppercase; letter-spacing:1px; }
1433
+ .ma-modal-body { padding:16px 20px 20px; display:flex; flex-direction:column; gap:14px; }
1434
+ .ma-modal-info { background:var(--surface-hi); border:1px solid var(--border); border-radius:8px; padding:10px 14px; display:flex; flex-direction:column; gap:8px; }
1435
+ .ma-modal-row { display:flex; justify-content:space-between; align-items:center; }
1436
+ .ma-modal-lbl { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); text-transform:uppercase; letter-spacing:1px; }
1437
+ .ma-modal-val { font-family:var(--mono); font-size:11px; color:var(--text-mid); }
1438
+ .ma-modal-val.ok { color:var(--green); }
1439
+ .ma-modal-val.warn { color:#fbbf24; }
1440
+ .ma-field-label { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); text-transform:uppercase; letter-spacing:1.5px; margin-bottom:4px; }
1441
+ .ma-field-input { width:100%; padding:7px 10px; border:1px solid var(--border-hi); border-radius:6px; background:var(--surface-hi); color:var(--text-hi); font-family:var(--mono); font-size:11px; box-sizing:border-box; outline:none; }
1442
+ .ma-field-input:focus { border-color:var(--accent); }
1443
+ .ma-steps { background:var(--surface-hi); border:1px solid var(--border); border-radius:8px; padding:8px 10px; max-height:130px; overflow-y:auto; display:flex; flex-direction:column; gap:5px; }
1444
+ .ma-step { display:flex; align-items:center; gap:8px; }
1445
+ .ma-step-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; }
1446
+ .ma-step-dot.ok { background:var(--green); }
1447
+ .ma-step-dot.err { background:oklch(60% 0.18 25); }
1448
+ .ma-step-txt { font-family:var(--mono); font-size:10px; color:var(--text-lo); }
1449
+ .ma-step-txt.ok { color:var(--green); }
1450
+ .ma-step-txt.err { color:oklch(60% 0.18 25); }
1451
+ .ma-btns { display:flex; gap:8px; flex-wrap:wrap; }
1452
+ .ma-btn { padding:6px 14px; border-radius:6px; border:none; font-family:var(--mono); font-size:11px; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:5px; transition:opacity 0.15s; }
1453
+ .ma-btn:disabled { opacity:0.45; cursor:default; }
1454
+ .ma-btn-primary { background:var(--accent); color:#fff; }
1455
+ .ma-btn-primary:hover:not(:disabled) { opacity:0.85; }
1456
+ .ma-btn-secondary { background:var(--surface-hi); color:var(--text-mid); border:1px solid var(--border-hi); }
1457
+ .ma-btn-secondary:hover:not(:disabled) { border-color:var(--accent); color:var(--accent); }
1458
+ .ma-btn-danger { background:oklch(17% 0.015 25); color:oklch(60% 0.18 25); border:1px solid oklch(60% 0.18 25 / 0.3); }
1459
+ .ma-btn-danger:hover:not(:disabled) { opacity:0.85; }
1460
+ .ma-btn-ghost { background:none; color:var(--text-lo); border:1px solid var(--border); }
1461
+ .ma-btn-ghost:hover:not(:disabled) { color:var(--text-mid); }
1462
+ .ma-msg { font-family:var(--mono); font-size:11px; padding:7px 12px; border-radius:6px; }
1463
+ .ma-msg.ok { background:oklch(65% 0.15 150 / 0.08); border:1px solid oklch(65% 0.15 150 / 0.25); color:var(--green); }
1464
+ .ma-msg.err { background:oklch(60% 0.18 25 / 0.08); border:1px solid oklch(60% 0.18 25 / 0.25); color:oklch(60% 0.18 25); }
1465
+ .ma-method-tabs { display:flex; gap:6px; flex-wrap:wrap; }
1466
+ .ma-method-tab { font-family:var(--mono); font-size:10px; padding:3px 10px; border-radius:4px; background:var(--surface-hi); border:1px solid var(--border); color:var(--text-lo); cursor:pointer; }
1467
+ .ma-method-tab.active { background:var(--accent-dim); border-color:var(--accent); color:var(--accent); }
1468
+ .ma-browser-hint { font-size:11px; color:var(--text-lo); line-height:1.5; }
1469
+
1470
+ /* ── playbook runs view ────────────────────────────────────── */
1471
+ .wf-run-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 10px; }
1472
+ .wf-run-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
1473
+ .wf-run-name { font-size: 13px; font-weight: 600; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1474
+ .wf-run-id { font-size: 10px; color: var(--text-xs); font-family: var(--mono); }
1475
+ .wf-status { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px; letter-spacing: 0.04em; flex-shrink: 0; }
1476
+ .wf-status.s-running { background: oklch(72% 0.18 75 / 0.15); color: var(--accent); }
1477
+ .wf-status.s-completed{ background: oklch(65% 0.15 150 / 0.15); color: var(--green); }
1478
+ .wf-status.s-failed { background: oklch(60% 0.18 25 / 0.15); color: var(--red); }
1479
+ .wf-status.s-stopped { background: oklch(42% 0.006 75 / 0.15); color: var(--text-lo); }
1480
+ .wf-run-meta { display: flex; gap: 16px; font-size: 11px; color: var(--text-lo); }
1481
+ .wf-run-meta span { display: flex; align-items: center; gap: 4px; }
1482
+ .wf-run-error { font-size: 11px; color: var(--red); margin-top: 6px; font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1483
+ .wf-progress-bar { height: 3px; background: var(--border); border-radius: 2px; margin-top: 8px; overflow: hidden; }
1484
+ .wf-progress-bar-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.3s; }
1485
+ #wf-runs-list .loading-txt, #wf-defs-list .loading-txt { padding: 40px 0; text-align: center; color: var(--text-lo); }
1486
+ .wf-def-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 8px; display: flex; align-items: flex-start; gap: 14px; cursor: default; transition: border-color 0.12s; }
1487
+ .wf-def-card:hover { border-color: var(--accent); }
1488
+ .wf-def-icon { font-size: 22px; flex-shrink: 0; margin-top: 1px; }
1489
+ .wf-def-body { flex: 1; min-width: 0; }
1490
+ .wf-def-name { font-size: 13px; font-weight: 600; color: var(--text-hi); margin-bottom: 2px; }
1491
+ .wf-def-desc { font-size: 12px; color: var(--text-lo); margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1492
+ .wf-def-meta { display: flex; gap: 10px; flex-wrap: wrap; }
1493
+ .wf-def-tag { font-size: 10px; padding: 2px 7px; border-radius: 8px; background: var(--surface-hi); color: var(--text-lo); font-family: var(--mono); }
1494
+ .wf-def-run-btn { font-size: 11px; padding: 4px 10px; background: var(--accent-dim); color: var(--accent); border: 1px solid oklch(72% 0.18 75 / 0.3); border-radius: 5px; cursor: pointer; white-space: nowrap; flex-shrink: 0; margin-top: 2px; }
1495
+ .wf-def-run-btn:hover { background: oklch(72% 0.18 75 / 0.2); }
1496
+ #wf-lib-empty { padding: 40px 0; text-align: center; color: var(--text-lo); font-size: 13px; }
1497
+ #wf-empty { padding: 40px 0; text-align: center; color: var(--text-lo); font-size: 13px; }
1498
+
1342
1499
  /* Mastermind overlay */
1343
1500
  #mm-overlay { display: none; position: fixed; top: 0; right: 0; bottom: 0; width: min(680px, 100vw); background: var(--surface); border-left: 1px solid oklch(65% 0.16 295 / 0.3); z-index: 500; flex-direction: column; box-shadow: -8px 0 32px oklch(0% 0 0 / 0.4); transition: transform 0.3s cubic-bezier(0.16,1,0.3,1); transform: translateX(100%); }
1344
1501
  #mm-overlay.open { display: flex; transform: translateX(0); }
@@ -1409,6 +1566,10 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1409
1566
  <div class="nav-item" data-view="monograph">
1410
1567
  <span class="ico">⬡</span><span class="lbl">Monograph</span>
1411
1568
  </div>
1569
+ <div class="nav-item" data-view="playbooks">
1570
+ <span class="ico">⚡</span><span class="lbl">Playbooks</span>
1571
+ <span class="bdg" id="bdg-playbooks">—</span>
1572
+ </div>
1412
1573
  </div>
1413
1574
  </div>
1414
1575
  <div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
@@ -1838,6 +1999,7 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1838
1999
  <button class="btn" onclick="v2ExportOrg()" title="Export org as JSON file" style="color:var(--text-lo);border-color:var(--border)">Export</button>
1839
2000
  <button class="btn" onclick="v2ImportOrgTrigger()" title="Import org from JSON file" style="color:var(--text-lo);border-color:var(--border)">Import</button>
1840
2001
  <button class="btn" id="org-copy-btn" onclick="v2ShowCopyOrgDialog()" style="color:var(--accent);border-color:var(--accent)">Copy to…</button>
2002
+ <button class="btn" id="org-mark-complete-btn" onclick="v2MarkOrgComplete()" style="display:none;color:oklch(70% 0.15 25);border-color:oklch(60% 0.15 25 / 0.4)" title="Write run:complete to clear STALE state">Mark Complete</button>
1841
2003
  <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>
1842
2004
  <button class="btn" id="org-delete-btn" onclick="v2DeleteOrg()" title="Permanently delete this org and all its data" style="color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Delete</button>
1843
2005
  </div>
@@ -2080,6 +2242,83 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2080
2242
  </div>
2081
2243
  </div>
2082
2244
 
2245
+ <!-- PLAYBOOKS -->
2246
+ <div class="view" id="view-playbooks">
2247
+ <div class="vscroll">
2248
+ <div class="pg-title">Playbooks</div>
2249
+ <div class="pg-sub">Browser automation — connected systems &amp; run history</div>
2250
+ <!-- Tab bar -->
2251
+ <div class="mg-tab-bar" style="margin-top:14px">
2252
+ <button class="odt-btn active" data-wftab="systems" onclick="wfSwitchTab('systems')">Systems</button>
2253
+ <button class="odt-btn" data-wftab="library" onclick="wfSwitchTab('library')">Library</button>
2254
+ <button class="odt-btn" data-wftab="builder" onclick="wfSwitchTab('builder')">Builder</button>
2255
+ <button class="odt-btn" data-wftab="runs" onclick="wfSwitchTab('runs')">Runs</button>
2256
+ </div>
2257
+
2258
+ <!-- TAB: SYSTEMS -->
2259
+ <div class="wf-pane active" id="wf-tab-systems">
2260
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;margin-top:4px">
2261
+ <button class="btn" onclick="loadPlatformSessions()">↺ Refresh</button>
2262
+ </div>
2263
+ <div id="wf-systems-grid"></div>
2264
+ </div>
2265
+
2266
+ <!-- TAB: LIBRARY -->
2267
+ <div class="wf-pane" id="wf-tab-library">
2268
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;margin-top:4px">
2269
+ <button class="btn" onclick="loadWorkflowDefs()">↺ Refresh</button>
2270
+ <span style="font-size:11px;color:var(--text-lo)">Saved in .monomind/playbooks/</span>
2271
+ </div>
2272
+ <div id="wf-defs-list"><div class="loading-txt">Loading…</div></div>
2273
+ </div>
2274
+
2275
+ <!-- TAB: RUNS -->
2276
+ <div class="wf-pane" id="wf-tab-runs">
2277
+ <div style="display:flex;gap:8px;margin-bottom:16px;margin-top:4px;align-items:center">
2278
+ <button class="btn" onclick="loadWorkflowRuns()">↺ Refresh</button>
2279
+ <select id="wf-status-filter" onchange="renderWorkflowRuns()" style="background:var(--surface);color:var(--text-hi);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:12px">
2280
+ <option value="">All statuses</option>
2281
+ <option value="running">Running</option>
2282
+ <option value="completed">Completed</option>
2283
+ <option value="failed">Failed</option>
2284
+ <option value="stopped">Stopped</option>
2285
+ </select>
2286
+ <span id="wf-count" style="color:var(--text-lo);font-size:12px"></span>
2287
+ </div>
2288
+ <div id="wf-runs-list"><div class="loading-txt">Loading…</div></div>
2289
+ </div>
2290
+
2291
+ <!-- TAB: BUILDER -->
2292
+ <div id="wf-tab-builder">
2293
+ <div id="pb-toolbar">
2294
+ <button class="btn" onclick="pbNew()" title="New playbook">+ New</button>
2295
+ <input id="pb-name-input" type="text" placeholder="Playbook name…" oninput="pbState.playbook.name=this.value">
2296
+ <button class="btn" onclick="pbLoad()" title="Load a playbook from Library">⬆ Load</button>
2297
+ <button class="btn" onclick="pbSave()" title="Save to .monomind/playbooks/">💾 Save</button>
2298
+ <button class="btn" onclick="pbRun()" title="Run via monobrowse dashboard" style="color:var(--accent);border-color:var(--accent)">▶ Run</button>
2299
+ <button class="btn" onclick="pbImportJSON()" title="Import from JSON">⬇ Import</button>
2300
+ <button class="btn" onclick="pbExportJSON()" title="Export to JSON">⬆ Export</button>
2301
+ <span id="pb-run-status"></span>
2302
+ </div>
2303
+ <div id="pb-editor">
2304
+ <div id="pb-palette">
2305
+ <!-- populated by pbRenderPalette() -->
2306
+ </div>
2307
+ <div id="pb-canvas-wrap">
2308
+ <div id="pb-canvas">
2309
+ <svg id="pb-svg" width="100%" height="100%"><defs><marker id="pb-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="var(--text-xs)"/></marker></defs></svg>
2310
+ <div id="pb-empty"><div style="font-size:32px">⚡</div><div>Drag a node from the left panel onto the canvas</div><div style="font-size:11px">or click a node type to add it</div></div>
2311
+ </div>
2312
+ </div>
2313
+ <div id="pb-config">
2314
+ <div id="pb-config-title">Select a node</div>
2315
+ <div id="pb-config-body"></div>
2316
+ </div>
2317
+ </div>
2318
+ </div>
2319
+ </div>
2320
+ </div>
2321
+
2083
2322
  <!-- GLOBAL FEED -->
2084
2323
  <div class="view" id="view-global">
2085
2324
  <div class="vscroll">
@@ -2271,7 +2510,7 @@ function switchView(v, { updateHash = true } = {}) {
2271
2510
  el.classList.toggle('active', el.dataset.view === v));
2272
2511
  document.querySelectorAll('.view').forEach(el =>
2273
2512
  el.classList.toggle('active', el.id === 'view-' + v));
2274
- 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:'Global Agent Chat' };
2513
+ const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', playbooks:'Playbooks', global:'Global Feed', 'global-loops':'Global Loops', 'global-tokens':'Global Tokens', chat:'Global Agent Chat' };
2275
2514
  document.getElementById('view-title').textContent = titles[v] || v;
2276
2515
  const PROJECT = DIR ? shortPath(DIR) : 'monomind';
2277
2516
  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' };
@@ -2334,6 +2573,7 @@ function renderView(v) {
2334
2573
  if (v === 'memory') renderMemory();
2335
2574
  if (v === 'orgs') renderOrgs();
2336
2575
  if (v === 'monograph') loadMonograph();
2576
+ if (v === 'playbooks') { loadPlatformSessions(); loadWorkflowDefs(); loadWorkflowRuns(); }
2337
2577
  if (v === 'global') renderGlobalFeed();
2338
2578
  if (v === 'global-loops') renderGlobalLoops();
2339
2579
  if (v === 'global-tokens') renderGlobalTokens();
@@ -4299,6 +4539,12 @@ function appendChatViewEvent(ev, animate) {
4299
4539
  if (currentView === 'loops') renderLoops();
4300
4540
  } else if (ev.type === 'file:write') {
4301
4541
  el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
4542
+ } else if (ev.type === 'agent:edit' || ev.type === 'agent:write') {
4543
+ el = mkCVTool('EDIT', ev.payload || ev.file || ev.path || '', '', 'oklch(65% 0.20 270)', ts);
4544
+ } else if (ev.type === 'agent:bash') {
4545
+ el = mkCVTool('BASH', ev.payload || ev.command || '', ev.output || '', 'oklch(72% 0.18 40)', ts);
4546
+ } else if (ev.type === 'agent:browse') {
4547
+ el = mkCVTool('BROWSE', ev.payload || ev.url || '', '', 'oklch(65% 0.15 190)', ts);
4302
4548
  } else if (ev.type === 'org:agent:offline') {
4303
4549
  el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline' + (ev.org ? ' [' + esc(ev.org) + ']' : ''), ts);
4304
4550
  } else if (ev.type === 'org:error') {
@@ -4317,6 +4563,9 @@ function _cvExcerptText(ev) {
4317
4563
  if (t === 'intercom' || t === 'org:comms') return (ev.from || ev.role || '?') + ' → ' + (ev.to || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
4318
4564
  if (t === 'agent:message') return (ev.agent || ev.name || ev.role || '?') + ': ' + (ev.msg || ev.message || ev.content || '').slice(0, 80);
4319
4565
  if (t === 'agent:result') return (ev.agent || ev.from || '?') + ': ' + (ev.result || ev.msg || ev.message || ev.summary || '').slice(0, 80);
4566
+ if (t === 'agent:edit' || t === 'agent:write') return '✏ ' + (ev.payload || ev.file || ev.path || '').replace(/.*\//, '');
4567
+ if (t === 'agent:bash') { const c = ev.payload || ev.command || ''; return '$ ' + c.slice(0, 80); }
4568
+ if (t === 'agent:browse') return '🌐 ' + (ev.payload || ev.url || '').slice(0, 80);
4320
4569
  if (t === 'agent:spawn') return 'spawned ' + (ev.to || ev.agent || ev.role || '?') + (ev.task || ev.briefing ? ': ' + (ev.task || ev.briefing || '').slice(0, 50) : '');
4321
4570
  if (t === 'org:agent:online') return (ev.title || ev.role || '?') + ' online';
4322
4571
  if (t === 'org:agent:offline') return (ev.title || ev.role || '?') + ' offline';
@@ -4424,6 +4673,23 @@ function mkCVFileCard(name, filePath, size, ts) {
4424
4673
  return d;
4425
4674
  }
4426
4675
 
4676
+ function mkCVTool(tag, cmd, output, color, ts) {
4677
+ const d = document.createElement('div');
4678
+ d.className = 'cv-msg cv-tool';
4679
+ const shortCmd = cmd.length > 120 ? '…' + cmd.slice(-100) : cmd;
4680
+ const uid = 'tool-' + Math.random().toString(36).slice(2,8);
4681
+ const hasOutput = output && output.trim().length > 0;
4682
+ const shortOut = output && output.length > 400 ? output.slice(0, 400) + '…' : output;
4683
+ d.innerHTML = `<div class="cv-bub" style="border-left:2px solid ${color};align-items:flex-start;flex-direction:column;gap:3px">`+
4684
+ `<div style="display:flex;align-items:center;gap:6px;width:100%">`+
4685
+ `<span class="cv-etype" style="color:${color};flex-shrink:0">${tag}</span>`+
4686
+ `<span class="cv-text" style="font-family:var(--mono);font-size:10px;word-break:break-all">${esc(shortCmd)}</span>`+
4687
+ `<span class="cv-ts" style="margin-left:auto;flex-shrink:0">${ts}</span></div>`+
4688
+ (hasOutput ? `<pre id="${uid}" style="margin:2px 0 0 0;padding:6px 8px;background:oklch(6% 0.01 295);border-radius:4px;font-size:9.5px;line-height:1.5;color:oklch(70% 0.05 295);overflow:auto;max-height:200px;width:100%;box-sizing:border-box;white-space:pre-wrap;word-break:break-word">${esc(shortOut)}</pre>` : '')+
4689
+ `</div>`;
4690
+ return d;
4691
+ }
4692
+
4427
4693
  function mkCVSys(html, ts) {
4428
4694
  const d = document.createElement('div');
4429
4695
  d.className = 'cv-msg cv-sys';
@@ -5585,7 +5851,7 @@ function v2RenderOrgList() {
5585
5851
  <div class="oi-name">${esc(o.name)}</div>
5586
5852
  ${goalSnip ? `<div class="oi-goal">${esc(goalSnip)}</div>` : ''}
5587
5853
  <div class="oi-chips">
5588
- ${o.running ? '<span class="oi-chip live">LIVE</span>' : ''}
5854
+ ${o.running ? `<span class="oi-chip ${o.lastEventAt ? (Date.now()-o.lastEventAt<1800000?'live':'quiet') : 'quiet'}">${o.lastEventAt ? (Date.now()-o.lastEventAt<1800000?'🟢 LIVE':'🟡 QUIET') : '🟡 QUIET'}</span>` : ''}
5589
5855
  <span class="oi-chip">${rolesN} roles</span>
5590
5856
  <span class="oi-chip">${esc(o.topology || 'hierarchical')}</span>
5591
5857
  </div>
@@ -5659,15 +5925,30 @@ async function v2SelectOrg(name) {
5659
5925
  function v2UpdateOrgHeader(listOrg, data) {
5660
5926
  document.getElementById('odh-name').textContent = listOrg.name || _v2SelOrg || '—';
5661
5927
  const running = data ? (data.running ?? listOrg.running) : listOrg.running;
5928
+ const _lastEvAt = data ? (data.lastEventAt ?? listOrg.lastEventAt) : listOrg.lastEventAt;
5662
5929
  const badge = document.getElementById('odh-badge');
5663
- badge.textContent = running ? 'LIVE' : 'IDLE';
5664
- badge.className = 'odh-badge ' + (running ? 'live' : 'idle');
5930
+ const _mcBtn = document.getElementById('org-mark-complete-btn');
5931
+ if (!running) { badge.textContent = 'IDLE'; badge.className = 'odh-badge idle'; if (_mcBtn) _mcBtn.style.display = 'none'; }
5932
+ else if (!_lastEvAt) { badge.textContent = '🟡 QUIET'; badge.className = 'odh-badge quiet'; if (_mcBtn) _mcBtn.style.display = 'none'; }
5933
+ else { const _a = Date.now() - _lastEvAt; badge.textContent = _a < 1800000 ? '🟢 LIVE' : _a < 7200000 ? '🟡 QUIET' : '🔴 STALE'; badge.className = 'odh-badge ' + (_a < 1800000 ? 'live' : _a < 7200000 ? 'quiet' : 'stale'); if (_mcBtn) _mcBtn.style.display = _a >= 7200000 ? '' : 'none'; }
5665
5934
  const roles = data ? (Array.isArray(data.roles) ? data.roles.length : (data.roles || 0)) : (listOrg.roles || 0);
5666
5935
  document.getElementById('odh-roles').textContent = roles + ' role' + (roles !== 1 ? 's' : '');
5667
5936
  document.getElementById('odh-topo').textContent = (data?.topology || listOrg.topology || '—');
5668
5937
  document.getElementById('org-stop-btn').style.display = running ? '' : 'none';
5669
5938
  }
5670
5939
 
5940
+ window.v2MarkOrgComplete = async function() {
5941
+ if (!_v2SelOrg) return;
5942
+ const btn = document.getElementById('org-mark-complete-btn');
5943
+ if (btn) { btn.disabled = true; btn.textContent = 'Marking…'; }
5944
+ try {
5945
+ const r = await fetch('/api/orgs/' + encodeURIComponent(_v2SelOrg) + '/mark-complete', { method: 'POST' });
5946
+ const j = await r.json();
5947
+ if (!r.ok) { if (btn) { btn.textContent = 'Failed: ' + (j.error || r.status); setTimeout(() => { if (btn) btn.textContent = 'Mark Complete'; }, 3000); } }
5948
+ } catch (e) { if (btn) { btn.textContent = 'Error'; setTimeout(() => { if (btn) btn.textContent = 'Mark Complete'; }, 3000); } console.error('[mark-complete]', e); }
5949
+ finally { if (btn) { btn.disabled = false; btn.textContent = 'Mark Complete'; } }
5950
+ };
5951
+
5671
5952
  window.v2SwitchOrgTab = function(tab) {
5672
5953
  // Clear live interval when switching away from live tab
5673
5954
  if (tab !== 'live' && _orgLiveInterval) {
@@ -6294,7 +6575,7 @@ function v2RenderOrgHealth() {
6294
6575
  if (!pane) return;
6295
6576
  pane.innerHTML = `
6296
6577
  <div class="health-v2-grid">
6297
- <div class="hv2-cell"><div class="hv2-lbl">Status</div><div class="hv2-val ${(_v2OrgData?.running??listOrg.running)?'green':''}">${(_v2OrgData?.running??listOrg.running)?'LIVE':'IDLE'}</div></div>
6578
+ <div class="hv2-cell"><div class="hv2-lbl">Status</div><div class="hv2-val ${(()=>{const r=_v2OrgData?.running??listOrg.running;if(!r)return '';const a=_v2OrgData?.lastEventAt??listOrg.lastEventAt;return a&&(Date.now()-a)<1800000?'green':'amber';})()} ">${(()=>{const r=_v2OrgData?.running??listOrg.running;if(!r)return 'IDLE';const a=_v2OrgData?.lastEventAt??listOrg.lastEventAt;if(!a)return '🟡 QUIET';const d=Date.now()-a;return d<1800000?'🟢 LIVE':d<7200000?'🟡 QUIET':'🔴 STALE';})()}</div></div>
6298
6579
  <div class="hv2-cell"><div class="hv2-lbl">Roles</div><div class="hv2-val">${roles}</div></div>
6299
6580
  <div class="hv2-cell"><div class="hv2-lbl">Topology</div><div class="hv2-val" style="font-size:14px">${esc((_v2OrgData?.topology||'—').toUpperCase())}</div></div>
6300
6581
  ${health?.agents_active!=null?`<div class="hv2-cell"><div class="hv2-lbl">Agents</div><div class="hv2-val ${health.agents_active>0?'green':''}">${health.agents_active}</div></div>`:''}
@@ -6902,6 +7183,9 @@ let _odtChatCurrentId = null;
6902
7183
  let _odtChatCurrentAgent = 'all';
6903
7184
  let _odtChatSseSource = null;
6904
7185
  let _odtChatSeenKeys = new Set();
7186
+ let _odtOrgTailSse = null; // per-org lifecycle tail SSE (receives bash-written run events)
7187
+ let _odtOrgTailOffset = 0; // line offset for reconnect (supplied as ?since=N)
7188
+ let _odtOrgTailOrg = null; // org the tail is currently connected to
6905
7189
  let _odtChatForOrg = '';
6906
7190
  // Loop grouping: [S] sessions with same prompt + <20 min gap → one entry
6907
7191
  let _odtSessionGroups = {}; // groupId → {sessions, events, status, ts, _isGroup}
@@ -6954,18 +7238,30 @@ async function v2RenderOrgChat() {
6954
7238
  <div id="odt-chat-agent-bar" style="display:none">
6955
7239
  <span id="odt-chat-agent-lbl">AGENT</span>
6956
7240
  </div>
7241
+ <div id="odt-chat-mode-row" style="display:flex;gap:4px;padding:4px 8px;border-bottom:1px solid oklch(62% 0.2 186 / 0.12);flex-shrink:0">
7242
+ <button id="odt-mode-summary" class="odt-mode-btn" onclick="setOdtChatMode('summary')" title="Summary: spawn/complete/edit/comms">SUMMARY</button>
7243
+ <button id="odt-mode-detailed" class="odt-mode-btn" onclick="setOdtChatMode('detailed')" title="Detailed: adds bash/browse/reads">DETAILED</button>
7244
+ <button id="odt-mode-raw" class="odt-mode-btn" onclick="setOdtChatMode('raw')" title="Raw: all events">RAW</button>
7245
+ </div>
6957
7246
  <div id="odt-chat-excerpt" class="cv-excerpt-banner"></div>
6958
7247
  <div id="odt-chat-feed">
6959
7248
  <div id="odt-chat-empty">Select a session to see agent communications.<br><span style="font-size:10px;opacity:0.5">Sessions appear after the first /mastermind:runorg</span></div>
6960
7249
  </div>`;
6961
7250
  }
6962
7251
 
7252
+ // Restore saved chat mode (runs on every tab open so buttons stay in sync)
7253
+ initOdtChatMode();
7254
+
6963
7255
  // Reset state only when org changes (not on every tab-switch)
6964
7256
  if (_odtChatForOrg !== _v2SelOrg) {
6965
7257
  _odtChatForOrg = _v2SelOrg;
6966
7258
  _odtChatCurrentId = null;
6967
7259
  _odtChatCurrentAgent = 'all';
6968
7260
  _odtChatSeenKeys = new Set();
7261
+ // Reset per-org lifecycle tail so it reconnects to the new org on next chat render
7262
+ if (_odtOrgTailSse) { _odtOrgTailSse.close(); _odtOrgTailSse = null; }
7263
+ _odtOrgTailOffset = 0;
7264
+ _odtOrgTailOrg = null;
6969
7265
  // Clear sessions from the previous org so _odtLoadChatSessions doesn't snapshot them
6970
7266
  // into _prevSessions and risk contaminating the new org's event buffers. SSE-injected
6971
7267
  // sessions for the NEW org arriving during the upcoming fetch are still captured via
@@ -7048,6 +7344,9 @@ async function v2RenderOrgChat() {
7048
7344
  }
7049
7345
  }
7050
7346
  _odtConnectChatSSE();
7347
+ // Wire per-org streaming tail to pick up bash-written lifecycle events (run:start, run:cycle:complete, run:complete)
7348
+ // that bypass mastermind-stream. The dedup set in _odtHandleLiveEvent filters any duplicates.
7349
+ if (_v2SelOrg) _odtConnectOrgTail(_v2SelOrg);
7051
7350
  }
7052
7351
 
7053
7352
  async function _odtLoadChatSessions() {
@@ -7308,12 +7607,23 @@ function _odtPopulateChatSel() {
7308
7607
  // Update label
7309
7608
  const _lblEl = document.getElementById('odt-chat-sess-lbl');
7310
7609
  if (_lblEl) _lblEl.textContent = (hasRuns && hasSess) ? 'HISTORY' : hasRuns ? 'RUN' : 'SESSION';
7311
- // Auto-select running entry
7610
+ // Auto-select: prefer running entry; if already on a non-running entry and a running one
7611
+ // becomes available (e.g. mastermind sessions loaded after initial run-file fetch), upgrade.
7612
+ const runningRunGrp = runGroups.find(g => g.status === 'running');
7613
+ const runningSessGrp = sessGroups.find(g => g.status === 'running');
7614
+ const bestRunning = runningRunGrp || runningSessGrp;
7615
+ const currentGrp = _odtChatCurrentId
7616
+ ? (runGroups.find(g => g.id === _odtChatCurrentId) || sessGroups.find(g => g.id === _odtChatCurrentId))
7617
+ : null;
7618
+ const currentIsRunning = currentGrp?.status === 'running';
7312
7619
  if (!_odtChatCurrentId) {
7313
- const runningRunGrp = runGroups.find(g => g.status === 'running');
7314
- const runningSessGrp = sessGroups.find(g => g.status === 'running');
7315
- const first = runningRunGrp || runningSessGrp || runGroups[0] || sessGroups[0];
7620
+ // Pick: running entry first, then most-recent by timestamp across both run and session groups
7621
+ const allEntries = [...runGroups, ...sessGroups].sort((a, b) => (b.ts || 0) - (a.ts || 0));
7622
+ const first = bestRunning || allEntries[0];
7316
7623
  if (first) { sel.value = first.id; odtChatSelectSession(first.id); }
7624
+ } else if (!currentIsRunning && bestRunning && bestRunning.id !== _odtChatCurrentId) {
7625
+ // Upgrade from stale/complete auto-selection to the running entry now that it's loaded
7626
+ sel.value = bestRunning.id; odtChatSelectSession(bestRunning.id);
7317
7627
  }
7318
7628
  }
7319
7629
 
@@ -7658,8 +7968,36 @@ window.odtChatSelectAgent = function(name) {
7658
7968
  feed.scrollTop = feed.scrollHeight;
7659
7969
  };
7660
7970
 
7971
+ // Chat tab render mode (Summary/Detailed/Raw) — persists to localStorage
7972
+ let _odtChatMode = localStorage.getItem('odt-chat-mode') || 'detailed';
7973
+
7974
+ function setOdtChatMode(mode) {
7975
+ _odtChatMode = mode;
7976
+ localStorage.setItem('odt-chat-mode', mode);
7977
+ document.querySelectorAll('.odt-mode-btn').forEach(b => b.classList.remove('active'));
7978
+ const btn = document.getElementById('odt-mode-' + mode);
7979
+ if (btn) btn.classList.add('active');
7980
+ }
7981
+
7982
+ function initOdtChatMode() {
7983
+ const saved = localStorage.getItem('odt-chat-mode') || 'detailed';
7984
+ setOdtChatMode(saved);
7985
+ }
7986
+
7661
7987
  function _odtAppendEvent(ev, animate) {
7662
7988
  if (!_odtChatAgentMatches(ev)) return;
7989
+ // Filter by render mode
7990
+ const _evType = ev.type || '';
7991
+ const _isSummaryType = _evType === 'agent:spawn' || _evType === 'agent:complete' ||
7992
+ _evType === 'agent:edit' || _evType === 'org:comms' || _evType === 'org:error' ||
7993
+ _evType === 'run:start' || _evType === 'run:complete' || _evType === 'org:start' ||
7994
+ _evType === 'org:checkpoint' || _evType === 'run:cycle:complete' || _evType === 'org:complete' || _evType === 'org:stop';
7995
+ const _isDetailedType = _isSummaryType || _evType === 'agent:bash' ||
7996
+ _evType === 'agent:browse' || _evType === 'agent:read:batch' || _evType === 'agent:usage' ||
7997
+ _evType === 'agent:result' || _evType === 'file:write' || _evType === 'org:agent:online';
7998
+ if (_odtChatMode === 'summary' && !_isSummaryType) return;
7999
+ if (_odtChatMode === 'detailed' && !_isDetailedType) return;
8000
+ // raw: show everything
7663
8001
  const feed = document.getElementById('odt-chat-feed');
7664
8002
  if (!feed) return;
7665
8003
  // Capture scroll position BEFORE appending: only auto-scroll for live events when
@@ -7724,6 +8062,12 @@ function _odtAppendEvent(ev, animate) {
7724
8062
  el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ''), ts);
7725
8063
  } else if (ev.type === 'loop:hil:resolved') {
7726
8064
  el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ''), ts);
8065
+ } else if (ev.type === 'agent:edit' || ev.type === 'agent:write') {
8066
+ el = mkCVTool('EDIT', ev.payload || ev.file || ev.path || '', '', 'oklch(65% 0.20 270)', ts);
8067
+ } else if (ev.type === 'agent:bash') {
8068
+ el = mkCVTool('BASH', ev.payload || ev.command || '', ev.output || '', 'oklch(72% 0.18 40)', ts);
8069
+ } else if (ev.type === 'agent:browse') {
8070
+ el = mkCVTool('BROWSE', ev.payload || ev.url || '', '', 'oklch(65% 0.15 190)', ts);
7727
8071
  } else if (ev.type === 'org:error') {
7728
8072
  el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
7729
8073
  el.classList.add('cv-err');
@@ -7759,6 +8103,28 @@ function _odtConnectChatSSE() {
7759
8103
  };
7760
8104
  }
7761
8105
 
8106
+ function _odtConnectOrgTail(orgName) {
8107
+ if (!orgName || (_odtOrgTailSse !== null && _odtOrgTailOrg === orgName)) return;
8108
+ if (_odtOrgTailSse) { _odtOrgTailSse.close(); _odtOrgTailSse = null; }
8109
+ _odtOrgTailOrg = orgName;
8110
+ const _tailUrl = '/api/orgs/' + encodeURIComponent(orgName) + '/runs/current/stream?since=' + _odtOrgTailOffset;
8111
+ _odtOrgTailSse = new EventSource(_tailUrl);
8112
+ _odtOrgTailSse.onmessage = e => {
8113
+ try {
8114
+ const ev = JSON.parse(e.data);
8115
+ if (ev && ev.type === 'stream:replay-done') { _odtOrgTailOffset = ev.count || _odtOrgTailOffset; return; }
8116
+ _odtOrgTailOffset++;
8117
+ _odtHandleLiveEvent(ev);
8118
+ } catch (_) {}
8119
+ };
8120
+ _odtOrgTailSse.onerror = () => {
8121
+ const _src = _odtOrgTailSse;
8122
+ _odtOrgTailSse = null;
8123
+ if (_src) _src.close();
8124
+ setTimeout(() => { if (_v2SelOrg === orgName && _odtOrgTailOrg === orgName) _odtConnectOrgTail(orgName); }, 5000);
8125
+ };
8126
+ }
8127
+
7762
8128
  function _odtHandleLiveEvent(ev) {
7763
8129
  if (ev?.project && DIR && ev.project !== DIR) return;
7764
8130
  // Deduplicate SSE reconnect replays (server replays last 50 events on every reconnect)
@@ -10107,6 +10473,1278 @@ let _mgWikiFilter = 'all';
10107
10473
  let _mgWikiMode = 'all';
10108
10474
  let _mgWikiSearchTimer = null;
10109
10475
 
10476
+ /* ── Playbook View ─────────────────────────────────────────── */
10477
+ // monoagent connections state
10478
+ let _maPlatforms = []; // full registry from monoes
10479
+ let _maConns = []; // active API/OAuth connections
10480
+ let _maSessions = []; // browser sessions
10481
+ let _maSelected = null; // currently open modal platform def
10482
+ let _maLoginPoll = null; // active login poll interval (cleared on modal close)
10483
+
10484
+ function wfSwitchTab(tab) {
10485
+ document.querySelectorAll('.wf-pane, #wf-tab-builder').forEach(p => p.classList.remove('active'));
10486
+ document.querySelectorAll('[data-wftab]').forEach(b => b.classList.remove('active'));
10487
+ document.getElementById('wf-tab-' + tab).classList.add('active');
10488
+ document.querySelector('[data-wftab="' + tab + '"]').classList.add('active');
10489
+ if (tab === 'systems') loadPlatformSessions();
10490
+ if (tab === 'library') loadWorkflowDefs();
10491
+ if (tab === 'runs') loadWorkflowRuns();
10492
+ if (tab === 'builder') pbInit();
10493
+ }
10494
+
10495
+ let _wfDefs = [];
10496
+ async function loadWorkflowDefs() {
10497
+ const el = document.getElementById('wf-defs-list');
10498
+ if (!el) return;
10499
+ el.innerHTML = '<div class="loading-txt">Loading…</div>';
10500
+ try {
10501
+ _wfDefs = await apiFetch('/api/workflow-defs');
10502
+ } catch { _wfDefs = []; }
10503
+ if (!_wfDefs.length) {
10504
+ el.innerHTML = `<div id="wf-lib-empty">No playbooks saved yet.<br><span style="font-size:12px;color:var(--text-xs)">Create one: <code style="font-family:var(--mono);color:var(--accent)">npx monomind browse playbook create my-playbook</code></span></div>`;
10505
+ return;
10506
+ }
10507
+ el.innerHTML = _wfDefs.map(d => {
10508
+ const ago = relativeTime(d.modifiedAt);
10509
+ const paramTags = (d.params || []).map(p => `<span class="wf-def-tag">${esc(p)}</span>`).join('');
10510
+ return `<div class="wf-def-card">
10511
+ <div class="wf-def-icon">⚡</div>
10512
+ <div class="wf-def-body">
10513
+ <div class="wf-def-name">${esc(d.name || d.id || d.file)}</div>
10514
+ ${d.description ? `<div class="wf-def-desc">${esc(d.description)}</div>` : ''}
10515
+ <div class="wf-def-meta">
10516
+ <span class="wf-def-tag">${esc(d.nodeCount)} node${d.nodeCount !== 1 ? 's' : ''}</span>
10517
+ ${paramTags}
10518
+ <span class="wf-def-tag" style="color:var(--text-xs)">${esc(ago)}</span>
10519
+ <span class="wf-def-tag" style="font-size:9px;color:var(--text-xs)">${esc(d.file)}</span>
10520
+ </div>
10521
+ </div>
10522
+ <div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
10523
+ <button class="wf-def-run-btn" title="Copy run command" onclick="navigator.clipboard?.writeText('npx monomind browse playbook run .monomind/playbooks/${esc(d.file)}').then(()=>this.textContent='Copied!').catch(()=>{});setTimeout(()=>this.textContent='▶ Run',1500)">▶ Run</button>
10524
+ <button class="wf-def-run-btn" style="font-size:10px" title="Open in Builder" onclick="pbLoadDef(${JSON.stringify(d.id)});wfSwitchTab('builder')">✎ Edit</button>
10525
+ </div>
10526
+ </div>`;
10527
+ }).join('');
10528
+ }
10529
+
10530
+ async function loadPlatformSessions() {
10531
+ const grid = document.getElementById('wf-systems-grid');
10532
+ if (!grid) return;
10533
+ grid.innerHTML = '<div class="loading-txt">Loading…</div>';
10534
+ try {
10535
+ const [plats, conns] = await Promise.all([
10536
+ fetch('/api/monoagent/platforms').then(r => r.json()).catch(() => []),
10537
+ fetch('/api/monoagent/connections').then(r => r.json()).catch(() => ({ connections: [], sessions: [] })),
10538
+ ]);
10539
+ _maPlatforms = Array.isArray(plats) ? plats : [];
10540
+ _maConns = Array.isArray(conns.connections) ? conns.connections : [];
10541
+ _maSessions = Array.isArray(conns.sessions) ? conns.sessions : [];
10542
+ renderSystems();
10543
+ } catch (e) {
10544
+ grid.innerHTML = '<div class="loading-txt">Failed to load systems.</div>';
10545
+ }
10546
+ }
10547
+
10548
+ function relativeTime(ts) {
10549
+ if (!ts) return '—';
10550
+ const s = Math.floor((Date.now() - ts) / 1000);
10551
+ if (s < 60) return 'just now';
10552
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
10553
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
10554
+ return Math.floor(s / 86400) + 'd ago';
10555
+ }
10556
+
10557
+ const MA_CATEGORY_ORDER = ['social', 'service', 'communication', 'database'];
10558
+ const MA_CATEGORY_LABELS = { social: 'Social', service: 'Services & APIs', communication: 'Communication', database: 'Databases' };
10559
+ const BROWSER_PLATFORMS = new Set(['instagram', 'linkedin', 'x', 'tiktok', 'gemini']);
10560
+
10561
+ function maResolveConn(pid) {
10562
+ pid = pid.toLowerCase();
10563
+ if (BROWSER_PLATFORMS.has(pid)) {
10564
+ const now = Date.now();
10565
+ const s = _maSessions.find(x => (x.Platform || '').toLowerCase() === pid);
10566
+ if (!s) return null;
10567
+ const expiry = new Date(s.Expiry).getTime();
10568
+ return { _type: 'session', id: String(s.ID), account: s.Username || '—', method: 'browser', status: expiry > now ? 'active' : 'expired' };
10569
+ }
10570
+ const c = _maConns.find(x => (x.platform || '').toLowerCase() === pid);
10571
+ if (!c) return null;
10572
+ return { _type: 'connection', id: c.id, account: c.account_id || c.label || '—', method: c.method || '—', status: c.status || 'active', lastTested: c.last_tested };
10573
+ }
10574
+
10575
+ function renderSystems() {
10576
+ const grid = document.getElementById('wf-systems-grid');
10577
+ if (!grid) return;
10578
+
10579
+ if (!_maPlatforms.length) {
10580
+ grid.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-xs);font-size:12px">
10581
+ <div style="font-size:28px;margin-bottom:8px">🔌</div>
10582
+ <div style="font-weight:600;color:var(--text-lo);margin-bottom:4px">monoagent not found</div>
10583
+ <div style="font-size:11px">Install monoes: <code style="color:var(--accent)">go install github.com/monoes/mono-agent/cmd/monoes@latest</code></div>
10584
+ </div>`;
10585
+ return;
10586
+ }
10587
+
10588
+ const groups = {};
10589
+ for (const p of _maPlatforms) {
10590
+ const cat = (p.Category || p.category || 'service').toLowerCase();
10591
+ if (!groups[cat]) groups[cat] = [];
10592
+ groups[cat].push(p);
10593
+ }
10594
+
10595
+ const cats = [...MA_CATEGORY_ORDER, ...Object.keys(groups).filter(c => !MA_CATEGORY_ORDER.includes(c))].filter(c => groups[c]);
10596
+ const totalConn = _maPlatforms.filter(p => maResolveConn(p.ID || p.id)).length;
10597
+
10598
+ let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
10599
+ <span style="font-size:11px;color:var(--text-xs)">${totalConn} / ${_maPlatforms.length} connected</span>
10600
+ <button class="ma-btn ma-btn-ghost" style="font-size:10px;padding:3px 10px" onclick="loadPlatformSessions()">↺ Refresh</button>
10601
+ </div>`;
10602
+
10603
+ for (const cat of cats) {
10604
+ const list = groups[cat];
10605
+ const catConn = list.filter(p => maResolveConn(p.ID || p.id)).length;
10606
+ html += `<div class="ma-cat-hdr">
10607
+ <span class="ma-cat-label">${MA_CATEGORY_LABELS[cat] || cat}</span>
10608
+ <div class="ma-cat-line"></div>
10609
+ <span class="ma-cat-count">${catConn}/${list.length}</span>
10610
+ </div><div class="ma-tiles">`;
10611
+ for (const p of list) {
10612
+ const pid = p.ID || p.id;
10613
+ const conn = maResolveConn(pid);
10614
+ const connected = conn && conn.status === 'active';
10615
+ const expired = conn && conn.status !== 'active';
10616
+ const cls = connected ? 'connected' : expired ? 'expired' : '';
10617
+ html += `<div class="ma-tile ${cls}" data-pid="${esc(pid)}" onclick="maOpenModal(this.dataset.pid)">
10618
+ <span class="ma-tile-icon">${esc(p.IconEmoji || p.iconEmoji || '🔌')}</span>
10619
+ <span class="ma-tile-name">${esc(p.Name || p.name || pid)}</span>
10620
+ <div style="display:flex;align-items:center;gap:4px">
10621
+ <span class="ma-tile-dot"></span>
10622
+ ${conn ? `<span class="ma-tile-acct">${esc(conn.account)}</span>` : ''}
10623
+ </div>
10624
+ </div>`;
10625
+ }
10626
+ html += `</div>`;
10627
+ }
10628
+
10629
+ grid.innerHTML = html;
10630
+ }
10631
+
10632
+ function maOpenModal(pid) {
10633
+ const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10634
+ if (!p) return;
10635
+ _maSelected = p;
10636
+ maRenderModal(p, maResolveConn(pid), []);
10637
+ }
10638
+
10639
+ function maRenderModal(p, conn, steps, selMethod, fields, state) {
10640
+ const pid = p.ID || p.id;
10641
+ const name = p.Name || p.name || pid;
10642
+ const emoji = p.IconEmoji || p.iconEmoji || '🔌';
10643
+ const methods = (p.Methods || p.methods || []);
10644
+ const method = selMethod || methods[0] || '';
10645
+ const isBrowser = method === 'browser';
10646
+ const connected = conn && conn.status === 'active';
10647
+ const expired = conn && conn.status !== 'active' && conn;
10648
+ state = state || {};
10649
+
10650
+ const existing = document.getElementById('ma-modal-overlay');
10651
+ if (existing) existing.remove();
10652
+
10653
+ const overlay = document.createElement('div');
10654
+ overlay.id = 'ma-modal-overlay';
10655
+ overlay.className = 'ma-modal-bg';
10656
+ overlay.addEventListener('click', e => { if (e.target === overlay) maCloseModal(); });
10657
+
10658
+ let bodyHtml = '';
10659
+
10660
+ if (conn) {
10661
+ // Connected state
10662
+ const statusColor = connected ? 'var(--green)' : '#fbbf24';
10663
+ bodyHtml = `
10664
+ <div class="ma-modal-info">
10665
+ ${[['Account', conn.account], ['Method', conn.method], ['Status', conn.status]].map(([l,v]) =>
10666
+ `<div class="ma-modal-row"><span class="ma-modal-lbl">${l}</span><span class="ma-modal-val ${v==='active'?'ok':v==='expired'?'warn':''}">${esc(v)}</span></div>`
10667
+ ).join('')}
10668
+ ${conn.lastTested ? `<div class="ma-modal-row"><span class="ma-modal-lbl">Last tested</span><span class="ma-modal-val">${esc(relativeTime(new Date(conn.lastTested).getTime()))}</span></div>` : ''}
10669
+ </div>
10670
+ ${state.testMsg ? `<div class="ma-msg ${state.testOk ? 'ok' : 'err'}">${esc(state.testMsg)}</div>` : ''}
10671
+ ${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}
10672
+ ${steps.length ? `<div class="ma-steps">${steps.map(s=>`<div class="ma-step"><span class="ma-step-dot ${s.kind}"></span><span class="ma-step-txt ${s.kind}">${esc(s.msg)}</span></div>`).join('')}</div>` : ''}
10673
+ <div class="ma-btns">
10674
+ <button class="ma-btn ma-btn-secondary" id="ma-test-btn" onclick="maTest('${pid}','${conn.id}')">Test</button>
10675
+ ${expired ? `<button class="ma-btn ma-btn-primary" onclick="maStartLogin('${pid}')">Log in again</button>` : ''}
10676
+ ${state.confirmDisc ? `
10677
+ <button class="ma-btn ma-btn-danger" onclick="maDisconnect('${pid}','${conn.id}','${conn._type}')">Yes, disconnect</button>
10678
+ <button class="ma-btn ma-btn-ghost" data-pid="${esc(pid)}" onclick="maOpenModal(this.dataset.pid)">Cancel</button>
10679
+ ` : `
10680
+ <button class="ma-btn ma-btn-danger" onclick="maConfirmDisconnect('${pid}')">Disconnect</button>
10681
+ <button class="ma-btn ma-btn-ghost" onclick="maCloseModal()">Close</button>
10682
+ `}
10683
+ </div>`;
10684
+ } else {
10685
+ // Not connected — show method tabs + connect form
10686
+ const methodTabsHtml = methods.length > 1 ? `<div class="ma-method-tabs">${methods.map(m =>
10687
+ `<button class="ma-method-tab ${m===method?'active':''}" data-pid="${esc(p.ID||p.id)}" data-method="${esc(m)}" onclick="maRenderModal(_maPlatforms.find(x=>(x.ID||x.id)===this.dataset.pid),null,[],this.dataset.method,{},{})">${esc(m)}</button>`
10688
+ ).join('')}</div>` : '';
10689
+
10690
+ let formHtml = '';
10691
+ if (isBrowser) {
10692
+ formHtml = `<div class="ma-browser-hint">A browser window will open. Log in to ${esc(name)} and the session will be saved automatically.</div>
10693
+ ${steps.length ? `<div class="ma-steps">${steps.map(s=>`<div class="ma-step"><span class="ma-step-dot ${s.kind}"></span><span class="ma-step-txt ${s.kind}">${esc(s.msg)}</span></div>`).join('')}</div>` : ''}
10694
+ ${state.running ? `<div style="display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-xs)"><span class="spinner" style="width:10px;height:10px;border-width:2px"></span>Waiting for login…</div>` : ''}
10695
+ ${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}`;
10696
+ } else {
10697
+ const fieldDefs = (p.Fields || p.fields || {})[method] || [];
10698
+ formHtml = fieldDefs.map(f => {
10699
+ const key = f.Key || f.key;
10700
+ const label = f.Label || f.label || key;
10701
+ const secret = f.Secret || f.secret;
10702
+ const req = f.Required || f.required;
10703
+ const val = (fields || {})[key] || '';
10704
+ return `<div>
10705
+ <div class="ma-field-label">${esc(label)}${req ? ' <span style="color:oklch(60% 0.18 25)">*</span>' : ''}</div>
10706
+ <input class="ma-field-input" type="${secret ? 'password' : 'text'}" data-key="${esc(key)}" value="${esc(val)}" autocomplete="${secret ? 'new-password' : 'off'}" placeholder="${esc(label)}">
10707
+ ${f.HelpURL || f.helpURL ? `<a href="#" style="font-size:9.5px;color:var(--accent);text-decoration:none" onclick="event.preventDefault()">How to get this ↗</a>` : ''}
10708
+ </div>`;
10709
+ }).join('') || `<div class="ma-browser-hint">No configuration required.</div>`;
10710
+ formHtml += `${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}
10711
+ ${state.ok ? `<div class="ma-msg ok">Connected!</div>` : ''}`;
10712
+ }
10713
+
10714
+ const connectBtn = isBrowser
10715
+ ? `<button class="ma-btn ma-btn-primary" id="ma-conn-btn" onclick="maStartLogin('${pid}')">Connect ${esc(name)}</button>`
10716
+ : `<button class="ma-btn ma-btn-primary" id="ma-conn-btn" data-pid="${esc(pid)}" data-method="${esc(method)}" onclick="maConnectApiKey(this.dataset.pid,this.dataset.method)">Connect</button>`;
10717
+
10718
+ bodyHtml = `
10719
+ ${methodTabsHtml}
10720
+ ${formHtml}
10721
+ <div class="ma-btns">
10722
+ ${state.ok ? '' : connectBtn}
10723
+ <button class="ma-btn ma-btn-ghost" onclick="loadPlatformSessions();maCloseModal()">Refresh</button>
10724
+ <button class="ma-btn ma-btn-ghost" onclick="maCloseModal()">Close</button>
10725
+ </div>`;
10726
+ }
10727
+
10728
+ overlay.innerHTML = `<div class="ma-modal">
10729
+ <div class="ma-modal-hdr">
10730
+ <div class="ma-modal-hdr-left">
10731
+ <span style="font-size:26px">${esc(emoji)}</span>
10732
+ <div>
10733
+ <div class="ma-modal-title">${esc(name)}</div>
10734
+ <div style="display:flex;align-items:center;gap:5px;margin-top:2px">
10735
+ <span style="width:6px;height:6px;border-radius:50%;background:${connected?'var(--green)':expired?'#fbbf24':'var(--border)'}"></span>
10736
+ <span class="ma-modal-status" style="color:${connected?'var(--green)':expired?'#fbbf24':'var(--text-xs)'}">${connected?'Connected':expired?'Session expired':'Not connected'}</span>
10737
+ </div>
10738
+ </div>
10739
+ </div>
10740
+ <button class="ma-btn ma-btn-ghost" style="padding:4px 8px" onclick="maCloseModal()">✕</button>
10741
+ </div>
10742
+ <div class="ma-modal-body">${bodyHtml}</div>
10743
+ </div>`;
10744
+
10745
+ document.body.appendChild(overlay);
10746
+ }
10747
+
10748
+ function maCloseModal() {
10749
+ if (_maLoginPoll) { clearInterval(_maLoginPoll); _maLoginPoll = null; }
10750
+ const el = document.getElementById('ma-modal-overlay');
10751
+ if (el) el.remove();
10752
+ _maSelected = null;
10753
+ }
10754
+
10755
+ function maConfirmDisconnect(pid) {
10756
+ const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10757
+ const conn = maResolveConn(pid);
10758
+ if (p && conn) maRenderModal(p, conn, [], null, {}, { confirmDisc: true });
10759
+ }
10760
+
10761
+ async function maStartLogin(pid) {
10762
+ const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10763
+ if (!p) return;
10764
+ const steps = [{ msg: 'Opening browser…', kind: '' }];
10765
+ maRenderModal(p, null, steps, 'browser', {}, { running: true });
10766
+ try {
10767
+ const r = await fetch('/api/monoagent/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: pid }) });
10768
+ if (!r.ok) throw new Error('HTTP ' + r.status);
10769
+ steps.push({ msg: 'Browser open — log in then come back', kind: 'ok' });
10770
+ maRenderModal(p, null, steps, 'browser', {}, { running: false });
10771
+ showToast('Browser opened', 'Log in to ' + (p.Name || pid) + ' and hit Refresh', 'ok');
10772
+ // Poll for completion
10773
+ let polls = 0;
10774
+ if (_maLoginPoll) clearInterval(_maLoginPoll);
10775
+ _maLoginPoll = setInterval(async () => {
10776
+ polls++;
10777
+ const d = await fetch('/api/monoagent/connections').then(r => r.json()).catch(() => null);
10778
+ if (d) { _maConns = d.connections || []; _maSessions = d.sessions || []; }
10779
+ const conn = maResolveConn(pid);
10780
+ if (conn || polls >= 18) {
10781
+ clearInterval(_maLoginPoll); _maLoginPoll = null;
10782
+ if (conn) { renderSystems(); maOpenModal(pid); showToast('Connected', (p.Name||pid) + ' session saved', 'ok'); }
10783
+ else { maRenderModal(p, null, [], 'browser', {}, { err: 'Login timed out. Close the browser tab and try again.' }); }
10784
+ }
10785
+ }, 5000);
10786
+ } catch (e) {
10787
+ maRenderModal(p, null, [], 'browser', {}, { err: e.message });
10788
+ }
10789
+ }
10790
+
10791
+ async function maConnectApiKey(pid, method) {
10792
+ const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10793
+ if (!p) return;
10794
+ const modal = document.getElementById('ma-modal-overlay');
10795
+ const inputs = modal ? modal.querySelectorAll('.ma-field-input') : [];
10796
+ const fields = {};
10797
+ inputs.forEach(inp => { fields[inp.dataset.key] = inp.value.trim(); });
10798
+ const btn = document.getElementById('ma-conn-btn');
10799
+ if (btn) { btn.disabled = true; btn.textContent = 'Connecting…'; }
10800
+ try {
10801
+ const r = await fetch('/api/monoagent/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: pid, method, fields }) });
10802
+ const d = await r.json();
10803
+ if (d.ok) {
10804
+ await loadPlatformSessions();
10805
+ maRenderModal(p, maResolveConn(pid), [], method, fields, { ok: true });
10806
+ showToast('Connected', (p.Name || pid) + ' connected', 'ok');
10807
+ } else {
10808
+ maRenderModal(p, null, [], method, fields, { err: d.stderr || d.error || 'Connection failed' });
10809
+ }
10810
+ } catch (e) {
10811
+ maRenderModal(p, null, [], method, fields, { err: e.message });
10812
+ }
10813
+ }
10814
+
10815
+ async function maTest(pid, connId) {
10816
+ const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10817
+ const conn = maResolveConn(pid);
10818
+ if (!p || !conn) return;
10819
+ const btn = document.getElementById('ma-test-btn');
10820
+ if (btn) { btn.disabled = true; btn.textContent = 'Testing…'; }
10821
+ try {
10822
+ const d = await fetch('/api/monoagent/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: connId }) }).then(r => r.json());
10823
+ maRenderModal(p, conn, [], null, {}, { testOk: d.ok, testMsg: d.ok ? '✓ Connection OK' : ('✗ ' + (d.error || 'Test failed')) });
10824
+ } catch (e) {
10825
+ maRenderModal(p, conn, [], null, {}, { testMsg: '✗ ' + e.message, testOk: false });
10826
+ }
10827
+ }
10828
+
10829
+ async function maDisconnect(pid, connId, type) {
10830
+ const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10831
+ if (!p) return;
10832
+ try {
10833
+ await fetch('/api/monoagent/disconnect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: connId, type }) });
10834
+ await loadPlatformSessions();
10835
+ maCloseModal();
10836
+ showToast('Disconnected', (p.Name || pid) + ' removed', 'ok');
10837
+ } catch (e) {
10838
+ showToast('Error', e.message, 'err');
10839
+ }
10840
+ }
10841
+
10842
+ let _wfRuns = [];
10843
+ async function loadWorkflowRuns() {
10844
+ document.getElementById('wf-runs-list').innerHTML = '<div class="loading-txt">Loading…</div>';
10845
+ try {
10846
+ const runs = await apiFetch('/api/workflow-runs');
10847
+ _wfRuns = Array.isArray(runs) ? runs : [];
10848
+ } catch (e) {
10849
+ _wfRuns = [];
10850
+ }
10851
+ const bdg = document.getElementById('bdg-playbooks');
10852
+ if (bdg) bdg.textContent = _wfRuns.length || '—';
10853
+ document.getElementById('wf-count').textContent = _wfRuns.length + ' run' + (_wfRuns.length !== 1 ? 's' : '');
10854
+ renderWorkflowRuns();
10855
+ }
10856
+
10857
+ function renderWorkflowRuns() {
10858
+ const filter = document.getElementById('wf-status-filter')?.value || '';
10859
+ const runs = filter ? _wfRuns.filter(r => r.status === filter) : _wfRuns;
10860
+ const el = document.getElementById('wf-runs-list');
10861
+ if (!runs.length) {
10862
+ el.innerHTML = '<div id="wf-empty">No playbook runs found. Run a playbook with:<br><code style="font-family:var(--mono);font-size:12px;color:var(--accent)">npx monomind browse playbook run &lt;file&gt;</code></div>';
10863
+ return;
10864
+ }
10865
+ el.innerHTML = runs.map(r => {
10866
+ const st = r.status || 'unknown';
10867
+ const elapsed = r.completedAt
10868
+ ? ((r.completedAt - r.startedAt) / 1000).toFixed(1) + 's'
10869
+ : r.startedAt ? ((Date.now() - r.startedAt) / 1000).toFixed(0) + 's' : '—';
10870
+ const pct = r.itemsTotal > 0 ? Math.round((r.itemsProcessed || 0) / r.itemsTotal * 100) : (st === 'completed' ? 100 : 0);
10871
+ const items = r.itemsTotal > 0 ? `${r.itemsProcessed || 0}/${r.itemsTotal}` : (r.itemsProcessed ? String(r.itemsProcessed) : '—');
10872
+ const started = r.startedAt ? new Date(r.startedAt).toLocaleString() : '—';
10873
+ return `<div class="wf-run-card">
10874
+ <div class="wf-run-header">
10875
+ <span class="wf-run-name" title="${esc(r.playbookName || r.playbookId || r.workflowName || r.workflowId || 'Playbook')}">${esc(r.playbookName || r.playbookId || r.workflowName || r.workflowId || 'Playbook')}</span>
10876
+ <span class="wf-status s-${esc(st)}">${esc(st.toUpperCase())}</span>
10877
+ </div>
10878
+ <div class="wf-run-meta">
10879
+ <span>⏱ ${esc(elapsed)}</span>
10880
+ <span>📦 ${esc(items)} items</span>
10881
+ <span>🕐 ${esc(started)}</span>
10882
+ <span class="wf-run-id">${esc((r.id || '').slice(0,12))}</span>
10883
+ </div>
10884
+ ${r.error ? `<div class="wf-run-error">✗ ${esc(r.error)}</div>` : ''}
10885
+ ${r.itemsTotal > 0 ? `<div class="wf-progress-bar"><div class="wf-progress-bar-fill" style="width:${pct}%"></div></div>` : ''}
10886
+ </div>`;
10887
+ }).join('');
10888
+ }
10889
+
10890
+ /* ═══════════════════════════════════════════════════════════════
10891
+ PLAYBOOK BUILDER
10892
+ ═══════════════════════════════════════════════════════════════ */
10893
+ const PB_CAT_COLOR = {
10894
+ 'Triggers':'#7c3aed','Control':'#0891b2','Data':'#d97706','Actions':'#059669',
10895
+ 'HTTP':'#ea580c','Comm':'#9333ea','Google':'#1d4ed8','Services':'#0f766e',
10896
+ };
10897
+
10898
+ const PB_NODE_DEFS = [
10899
+ // ── Triggers ──────────────────────────────────────────────────────────────
10900
+ { cat:'Triggers', type:'trigger.manual', label:'Manual Trigger', icon:'▶' },
10901
+ { cat:'Triggers', type:'trigger.schedule', label:'Schedule', icon:'⏰' },
10902
+ { cat:'Triggers', type:'trigger.webhook', label:'Webhook', icon:'🔗' },
10903
+ // ── Control ───────────────────────────────────────────────────────────────
10904
+ { cat:'Control', type:'core.if', label:'If / Branch', icon:'⑂' },
10905
+ { cat:'Control', type:'core.switch', label:'Switch', icon:'⑃' },
10906
+ { cat:'Control', type:'core.filter', label:'Filter', icon:'⚗' },
10907
+ { cat:'Control', type:'core.merge', label:'Merge', icon:'⊕' },
10908
+ { cat:'Control', type:'core.wait', label:'Wait', icon:'⏱' },
10909
+ { cat:'Control', type:'core.stop_error', label:'Stop & Error', icon:'⛔' },
10910
+ // ── Data ──────────────────────────────────────────────────────────────────
10911
+ { cat:'Data', type:'core.set', label:'Set Fields', icon:'✎' },
10912
+ { cat:'Data', type:'core.aggregate', label:'Aggregate', icon:'∑' },
10913
+ { cat:'Data', type:'core.sort', label:'Sort', icon:'⇅' },
10914
+ { cat:'Data', type:'core.limit', label:'Limit', icon:'✂' },
10915
+ { cat:'Data', type:'core.remove_duplicates', label:'Dedup', icon:'⊘' },
10916
+ { cat:'Data', type:'core.split_in_batches', label:'Split Batches', icon:'⋮' },
10917
+ { cat:'Data', type:'core.compare_datasets', label:'Compare', icon:'⟺' },
10918
+ { cat:'Data', type:'core.code', label:'Code (JS)', icon:'〈/〉' },
10919
+ { cat:'Data', type:'data.datetime', label:'Date & Time', icon:'📅' },
10920
+ { cat:'Data', type:'data.html', label:'HTML', icon:'📝' },
10921
+ { cat:'Data', type:'data.markdown', label:'Markdown', icon:'📄' },
10922
+ { cat:'Data', type:'data.xml', label:'XML', icon:'📋' },
10923
+ // ── Actions (monoplaybook builtins) ───────────────────────────────────────
10924
+ { cat:'Actions', type:'action.http', label:'HTTP Request', icon:'🌐' },
10925
+ { cat:'Actions', type:'action.log', label:'Log', icon:'📜' },
10926
+ { cat:'Actions', type:'action.save_file', label:'Save File', icon:'💾' },
10927
+ { cat:'Actions', type:'action.gemini_image', label:'Gemini Image', icon:'🖼' },
10928
+ // ── HTTP / Network ────────────────────────────────────────────────────────
10929
+ { cat:'HTTP', type:'http.request', label:'HTTP Request', icon:'⊹' },
10930
+ // ── Communications ────────────────────────────────────────────────────────
10931
+ { cat:'Comm', type:'comm.email_send', label:'Send Email', icon:'✉' },
10932
+ { cat:'Comm', type:'comm.slack', label:'Slack', icon:'💬' },
10933
+ { cat:'Comm', type:'comm.discord', label:'Discord', icon:'🎮' },
10934
+ { cat:'Comm', type:'comm.telegram', label:'Telegram', icon:'✈' },
10935
+ { cat:'Comm', type:'comm.twilio', label:'Twilio SMS', icon:'📱' },
10936
+ // ── Google (service.* — real monoplaybook handlers) ──────────────────────
10937
+ { cat:'Google', type:'service.gmail', defaultConfig:{operation:'send_message'}, label:'Gmail: Send', icon:'✉' },
10938
+ { cat:'Google', type:'service.gmail', defaultConfig:{operation:'list_messages'}, label:'Gmail: List', icon:'📬' },
10939
+ { cat:'Google', type:'service.google_drive', defaultConfig:{operation:'list_files'}, label:'Drive: List', icon:'📁' },
10940
+ { cat:'Google', type:'service.google_drive', defaultConfig:{operation:'download_file'}, label:'Drive: Download',icon:'⬇' },
10941
+ { cat:'Google', type:'service.google_drive', defaultConfig:{operation:'upload_file'}, label:'Drive: Upload', icon:'⬆' },
10942
+ { cat:'Google', type:'service.google_sheets',defaultConfig:{operation:'read_rows'}, label:'Sheets: Read', icon:'📊' },
10943
+ { cat:'Google', type:'service.google_sheets',defaultConfig:{operation:'append_rows'}, label:'Sheets: Append', icon:'➕' },
10944
+ // ── Services (service.* — real monoplaybook handlers) ────────────────────
10945
+ { cat:'Services', type:'service.github', defaultConfig:{operation:'list_issues'}, label:'GitHub: Issues', icon:'⊙' },
10946
+ { cat:'Services', type:'service.github', defaultConfig:{operation:'create_issue'}, label:'GitHub: Create', icon:'⊙' },
10947
+ { cat:'Services', type:'service.notion', defaultConfig:{operation:'query_database'}, label:'Notion: Query', icon:'📄' },
10948
+ { cat:'Services', type:'service.notion', defaultConfig:{operation:'create_page'}, label:'Notion: Create', icon:'📄' },
10949
+ { cat:'Services', type:'service.linear', defaultConfig:{operation:'list_issues'}, label:'Linear: Issues', icon:'◉' },
10950
+ { cat:'Services', type:'service.linear', defaultConfig:{operation:'create_issue'}, label:'Linear: Create', icon:'◉' },
10951
+ { cat:'Services', type:'service.airtable', defaultConfig:{operation:'list_records'}, label:'Airtable: List', icon:'⊞' },
10952
+ { cat:'Services', type:'service.airtable', defaultConfig:{operation:'create_record'}, label:'Airtable: Create', icon:'⊞' },
10953
+ { cat:'Services', type:'service.stripe', defaultConfig:{operation:'list_customers'}, label:'Stripe: Customers', icon:'💳' },
10954
+ { cat:'Services', type:'service.stripe', defaultConfig:{operation:'list_charges'}, label:'Stripe: Charges', icon:'💳' },
10955
+ ];
10956
+
10957
+ const PB_NODE_FIELDS = {
10958
+ // ── Triggers ──────────────────────────────────────────────────────────────
10959
+ 'trigger.manual': [],
10960
+ 'trigger.schedule': [
10961
+ { key:'cron', label:'Cron Expression', type:'text', placeholder:'*/5 * * * *', help:'Standard 5-field cron: min hour day month weekday' },
10962
+ { key:'timezone', label:'Timezone', type:'text', placeholder:'UTC' },
10963
+ ],
10964
+ 'trigger.webhook': [
10965
+ { key:'path', label:'Webhook Path', type:'text', placeholder:'/webhooks/my-playbook' },
10966
+ { key:'method', label:'HTTP Method', type:'select', options:['POST','GET','PUT','PATCH'] },
10967
+ { key:'secret', label:'HMAC Secret', type:'password', placeholder:'optional — verified via X-Signature header' },
10968
+ ],
10969
+ // ── Control ───────────────────────────────────────────────────────────────
10970
+ 'core.if': [{ key:'condition', label:'Condition', type:'textarea', placeholder:'{{ eq $json.active true }}', help:'Go template — items go to "true" or "false" handle' }],
10971
+ 'core.filter': [
10972
+ { key:'condition', label:'Condition', type:'textarea', placeholder:'{{ eq $json.status "todo" }}' },
10973
+ { key:'mode', label:'Mode', type:'select', options:['keep','remove'] },
10974
+ ],
10975
+ 'core.switch': [
10976
+ { key:'expression', label:'Expression', type:'text', placeholder:'{{$json.status}}' },
10977
+ { key:'cases', label:'Cases (JSON)', type:'textarea', placeholder:'[{"value":"active"},{"value":"closed"}]' },
10978
+ { key:'default_handle', label:'Default Handle',type:'text', placeholder:'default' },
10979
+ ],
10980
+ 'core.merge': [{ key:'mode', label:'Mode', type:'select', options:['append','first'] }],
10981
+ 'core.wait': [{ key:'duration', label:'Duration', type:'text', placeholder:'5s', help:'e.g. 5s, 2m, 1h' }],
10982
+ 'core.stop_error': [{ key:'message', label:'Error Message', type:'text', placeholder:'Stopped: condition not met' }],
10983
+ // ── Data ──────────────────────────────────────────────────────────────────
10984
+ 'core.set': [
10985
+ { key:'assignments', label:'Assignments (JSON)', type:'textarea', placeholder:'[{"name":"output","value":"{{$json.input}}"}]' },
10986
+ { key:'include_input', label:'Include Input', type:'select', options:['true','false'] },
10987
+ ],
10988
+ 'core.aggregate': [
10989
+ { key:'group_by', label:'Group By Field', type:'text', placeholder:'' },
10990
+ { key:'operations', label:'Operations', type:'textarea', placeholder:'[{"field":"amount","operation":"sum","output_field":"total"}]' },
10991
+ ],
10992
+ 'core.sort': [
10993
+ { key:'field', label:'Sort By', type:'text', placeholder:'name' },
10994
+ { key:'order', label:'Order', type:'select', options:['asc','desc'] },
10995
+ { key:'type', label:'Type', type:'select', options:['string','number','date'] },
10996
+ ],
10997
+ 'core.limit': [{ key:'max_items', label:'Max Items', type:'number', placeholder:'10' }],
10998
+ 'core.remove_duplicates': [
10999
+ { key:'field', label:'Field Key', type:'text', placeholder:'id' },
11000
+ { key:'keep', label:'Keep', type:'select', options:['first','last'] },
11001
+ ],
11002
+ 'core.split_in_batches': [{ key:'batch_size', label:'Batch Size', type:'number', placeholder:'10' }],
11003
+ 'core.compare_datasets': [
11004
+ { key:'key_field', label:'Key Field', type:'text', placeholder:'id' },
11005
+ { key:'split_at', label:'Split At Index', type:'number', placeholder:'' },
11006
+ ],
11007
+ 'core.code': [{ key:'code', label:'JavaScript', type:'textarea', placeholder:'// return array of items\nreturn items.map(item => ({ ...item.json }))' }],
11008
+ 'data.datetime': [
11009
+ { key:'operation', label:'Operation', type:'select', options:['format','parse','add','subtract','diff','now'] },
11010
+ { key:'field', label:'Source Field', type:'text', placeholder:'date' },
11011
+ { key:'input_format', label:'Input Format', type:'text', placeholder:'' },
11012
+ { key:'output_format', label:'Output Format', type:'text', placeholder:'2006-01-02T15:04:05Z07:00' },
11013
+ { key:'duration', label:'Duration', type:'text', placeholder:'24h' },
11014
+ { key:'output_field', label:'Output Field', type:'text', placeholder:'formatted_date' },
11015
+ ],
11016
+ 'data.html': [
11017
+ { key:'operation', label:'Operation', type:'select', options:['extract','extract_all','text','generate'] },
11018
+ { key:'field', label:'Source Field', type:'text', placeholder:'' },
11019
+ { key:'selector', label:'CSS Selector', type:'text', placeholder:'h1.title' },
11020
+ { key:'attribute', label:'Attribute', type:'text', placeholder:'href' },
11021
+ { key:'template', label:'HTML Template',type:'textarea',placeholder:'<p>{{.Title}}</p>' },
11022
+ ],
11023
+ 'data.markdown': [
11024
+ { key:'field', label:'Source Field', type:'text', placeholder:'' },
11025
+ { key:'output_field', label:'Output Field', type:'text', placeholder:'html' },
11026
+ ],
11027
+ 'data.xml': [
11028
+ { key:'operation', label:'Operation', type:'select', options:['parse','generate'] },
11029
+ { key:'field', label:'Source Field', type:'text', placeholder:'' },
11030
+ { key:'root_element', label:'Root Element', type:'text', placeholder:'root' },
11031
+ ],
11032
+ // ── Actions (monoplaybook builtins) ───────────────────────────────────────
11033
+ 'action.http': [
11034
+ { key:'url', label:'URL', type:'text', placeholder:'https://api.example.com/data' },
11035
+ { key:'method', label:'Method', type:'select', options:['GET','POST','PUT','PATCH','DELETE'] },
11036
+ { key:'body', label:'Body (JSON)', type:'textarea', placeholder:'{}' },
11037
+ { key:'responseField', label:'Save As Field', type:'text', placeholder:'response' },
11038
+ { key:'timeoutMs', label:'Timeout (ms)', type:'number', placeholder:'30000' },
11039
+ ],
11040
+ 'action.log': [
11041
+ { key:'label', label:'Label', type:'text', placeholder:'debug', help:'Prefix shown in console output' },
11042
+ ],
11043
+ 'action.save_file': [
11044
+ { key:'path', label:'Output Path', type:'text', placeholder:'./output/result.json' },
11045
+ { key:'field', label:'Field', type:'text', placeholder:'leave blank for full item' },
11046
+ { key:'encoding', label:'Encoding', type:'select', options:['utf8','base64','binary'] },
11047
+ ],
11048
+ 'action.gemini_image': [
11049
+ { key:'prompt', label:'Prompt', type:'textarea', placeholder:'A futuristic city skyline at dusk' },
11050
+ { key:'outputPath', label:'Output Path', type:'text', placeholder:'./output/image.png' },
11051
+ { key:'aspectRatio', label:'Aspect Ratio',type:'select', options:['1:1','16:9','9:16','4:3'] },
11052
+ { key:'apiKey', label:'API Key', type:'password', placeholder:'uses GEMINI_API_KEY env if blank' },
11053
+ { key:'model', label:'Model', type:'text', placeholder:'imagen-3.0-generate-001' },
11054
+ ],
11055
+ // ── HTTP / Network ────────────────────────────────────────────────────────
11056
+ 'http.request': [
11057
+ { key:'method', label:'Method', type:'select', options:['GET','POST','PUT','PATCH','DELETE','HEAD'] },
11058
+ { key:'url', label:'URL', type:'text', placeholder:'https://api.example.com/endpoint' },
11059
+ { key:'body_type', label:'Body Type', type:'select', options:['none','json','form','raw'] },
11060
+ { key:'body', label:'Body (JSON)', type:'textarea', placeholder:'' },
11061
+ { key:'auth_type', label:'Auth', type:'select', options:['none','bearer','basic','api_key'] },
11062
+ { key:'auth_api_key_value', label:'Token / Key', type:'password', placeholder:'' },
11063
+ { key:'response_format', label:'Response', type:'select', options:['json','text','binary'] },
11064
+ ],
11065
+ 'http.ftp': [
11066
+ { key:'host', label:'Host', type:'text', placeholder:'ftp.example.com' },
11067
+ { key:'port', label:'Port', type:'number', placeholder:'21' },
11068
+ { key:'username', label:'Username', type:'text', placeholder:'' },
11069
+ { key:'password', label:'Password', type:'password', placeholder:'' },
11070
+ { key:'remote_path', label:'Remote Path',type:'text', placeholder:'/' },
11071
+ { key:'operation', label:'Operation', type:'select', options:['list','download','upload','delete'] },
11072
+ ],
11073
+ 'http.ssh': [
11074
+ { key:'host', label:'Host', type:'text', placeholder:'server.example.com' },
11075
+ { key:'port', label:'Port', type:'number', placeholder:'22' },
11076
+ { key:'username', label:'Username', type:'text', placeholder:'' },
11077
+ { key:'password', label:'Password', type:'password', placeholder:'' },
11078
+ { key:'command', label:'Command', type:'textarea', placeholder:'ls -la /var/log' },
11079
+ ],
11080
+ 'system.execute_command': [
11081
+ { key:'command', label:'Command', type:'text', placeholder:'echo' },
11082
+ { key:'args', label:'Args (JSON)', type:'text', placeholder:'["hello world"]' },
11083
+ { key:'working_dir', label:'Working Dir', type:'text', placeholder:'' },
11084
+ { key:'timeout_seconds', label:'Timeout (s)', type:'number', placeholder:'30' },
11085
+ ],
11086
+ 'system.rss_read': [
11087
+ { key:'url', label:'Feed URL', type:'text', placeholder:'https://feeds.example.com/rss' },
11088
+ { key:'limit', label:'Max Items', type:'number', placeholder:'20' },
11089
+ ],
11090
+ // ── Communications ────────────────────────────────────────────────────────
11091
+ 'comm.email_send': [
11092
+ { key:'smtp_host', label:'SMTP Host', type:'text', placeholder:'smtp.gmail.com' },
11093
+ { key:'smtp_port', label:'SMTP Port', type:'number', placeholder:'587' },
11094
+ { key:'smtp_user', label:'Username', type:'text', placeholder:'user@example.com' },
11095
+ { key:'smtp_password', label:'Password', type:'password', placeholder:'' },
11096
+ { key:'from', label:'From', type:'text', placeholder:'sender@example.com' },
11097
+ { key:'to', label:'To', type:'text', placeholder:'recipient@example.com' },
11098
+ { key:'subject', label:'Subject', type:'text', placeholder:'Hello' },
11099
+ { key:'body', label:'Body', type:'textarea', placeholder:'Hi {{$json.name}},' },
11100
+ { key:'body_type', label:'Body Type', type:'select', options:['text','html'] },
11101
+ ],
11102
+ 'comm.slack': [
11103
+ { key:'webhook_url', label:'Webhook URL', type:'password', placeholder:'https://hooks.slack.com/services/…' },
11104
+ { key:'channel', label:'Channel', type:'text', placeholder:'#general' },
11105
+ { key:'text', label:'Message', type:'textarea', placeholder:'Hello from monoplaybook!' },
11106
+ { key:'username', label:'Bot Name', type:'text', placeholder:'Monoplaybook' },
11107
+ ],
11108
+ 'comm.discord': [
11109
+ { key:'webhook_url', label:'Webhook URL', type:'password', placeholder:'https://discord.com/api/webhooks/…' },
11110
+ { key:'content', label:'Message', type:'textarea', placeholder:'Hello from monoplaybook!' },
11111
+ { key:'username', label:'Bot Name', type:'text', placeholder:'Monoplaybook' },
11112
+ ],
11113
+ 'comm.telegram': [
11114
+ { key:'token', label:'Bot Token', type:'password', placeholder:'123456:ABC-DEF…' },
11115
+ { key:'chat_id', label:'Chat ID', type:'text', placeholder:'-100123456' },
11116
+ { key:'text', label:'Message', type:'textarea', placeholder:'Hello {{$json.name}}!' },
11117
+ { key:'parse_mode', label:'Format', type:'select', options:['text','Markdown','HTML'] },
11118
+ ],
11119
+ 'comm.twilio': [
11120
+ { key:'account_sid', label:'Account SID', type:'text', placeholder:'ACxxxx' },
11121
+ { key:'auth_token', label:'Auth Token', type:'password', placeholder:'' },
11122
+ { key:'from', label:'From Number', type:'text', placeholder:'+15551234567' },
11123
+ { key:'to', label:'To Number', type:'text', placeholder:'{{$json.phone}}' },
11124
+ { key:'body', label:'Message', type:'textarea', placeholder:'Your code is {{$json.code}}' },
11125
+ ],
11126
+ // ── Google ────────────────────────────────────────────────────────────────
11127
+ // ── Google (service.* handlers — consolidated with operation selector) ─────
11128
+ 'service.gmail': [
11129
+ { key:'operation', label:'Operation', type:'select', options:['send_message','list_messages','get_message','list_labels','trash_message'] },
11130
+ { key:'access_token', label:'OAuth Token', type:'password', placeholder:'ya29.…' },
11131
+ { key:'to', label:'To (send)', type:'text', placeholder:'recipient@example.com' },
11132
+ { key:'subject', label:'Subject (send)',type:'text', placeholder:'Hello from monoplaybook' },
11133
+ { key:'body', label:'Body (send)', type:'textarea', placeholder:'Hi there,' },
11134
+ { key:'body_type', label:'Body Type', type:'select', options:['text','html'] },
11135
+ { key:'query', label:'Search (list)',type:'text', placeholder:'from:alice is:unread' },
11136
+ { key:'max_results', label:'Max Results', type:'number', placeholder:'10' },
11137
+ { key:'message_id', label:'Message ID (get/trash)',type:'text', placeholder:'' },
11138
+ ],
11139
+ 'service.google_drive': [
11140
+ { key:'operation', label:'Operation', type:'select', options:['list_files','download_file','upload_file','get_file','create_folder','delete_file','share_file'] },
11141
+ { key:'access_token', label:'OAuth Token', type:'password', placeholder:'ya29.…' },
11142
+ { key:'query', label:'Search (list)',type:'text', placeholder:'name contains "report"' },
11143
+ { key:'file_id', label:'File ID (get/download/delete/share)',type:'text', placeholder:'1BxiMV…' },
11144
+ { key:'file_path', label:'Local Path (upload)',type:'text', placeholder:'/tmp/file.pdf' },
11145
+ { key:'file_name', label:'Name (upload/folder)',type:'text', placeholder:'report.pdf' },
11146
+ { key:'parent_folder_id', label:'Parent Folder ID', type:'text', placeholder:'' },
11147
+ { key:'mime_type', label:'MIME Type (upload)', type:'text', placeholder:'application/pdf' },
11148
+ ],
11149
+ 'service.google_sheets': [
11150
+ { key:'operation', label:'Operation', type:'select', options:['read_rows','append_rows','update_rows','clear_range'] },
11151
+ { key:'access_token', label:'OAuth Token', type:'password', placeholder:'ya29.…' },
11152
+ { key:'spreadsheet_id', label:'Spreadsheet ID', type:'text', placeholder:'1BxiMV…' },
11153
+ { key:'sheet', label:'Sheet Name', type:'text', placeholder:'Sheet1' },
11154
+ { key:'range', label:'Range', type:'text', placeholder:'A1:Z' },
11155
+ { key:'use_header_row', label:'Header Row (read)',type:'select', options:['true','false'] },
11156
+ ],
11157
+ // ── Services (service.* handlers) ─────────────────────────────────────────
11158
+ 'service.github': [
11159
+ { key:'operation', label:'Operation', type:'select', options:['list_issues','create_issue','get_issue','update_issue','list_repos','list_prs','create_pr','list_releases','create_release'] },
11160
+ { key:'token', label:'Access Token', type:'password', placeholder:'ghp_…' },
11161
+ { key:'owner', label:'Owner', type:'text', placeholder:'octocat' },
11162
+ { key:'repo', label:'Repo', type:'text', placeholder:'hello-world' },
11163
+ { key:'state', label:'State (list)', type:'select', options:['open','closed','all'] },
11164
+ { key:'title', label:'Title (create)',type:'text', placeholder:'Bug: something is broken' },
11165
+ { key:'body', label:'Body (create)', type:'textarea',placeholder:'Steps to reproduce…' },
11166
+ ],
11167
+ 'service.notion': [
11168
+ { key:'operation', label:'Operation', type:'select', options:['query_database','create_page','get_page','update_page','get_database','create_database','append_blocks'] },
11169
+ { key:'token', label:'Integration Token', type:'password', placeholder:'secret_…' },
11170
+ { key:'database_id', label:'Database ID', type:'text', placeholder:'xxxxxxxx-xxxx-…' },
11171
+ { key:'parent_id', label:'Parent Page/DB ID', type:'text', placeholder:'xxxxxxxx-xxxx-…' },
11172
+ { key:'page_id', label:'Page ID (get/update)', type:'text', placeholder:'xxxxxxxx-xxxx-…' },
11173
+ { key:'title', label:'Title (create)', type:'text', placeholder:'New Page' },
11174
+ { key:'properties', label:'Properties (JSON)', type:'textarea', placeholder:'{}' },
11175
+ { key:'filter', label:'Filter (JSON, query)', type:'textarea', placeholder:'{"property":"Status","select":{"equals":"Done"}}' },
11176
+ ],
11177
+ 'service.linear': [
11178
+ { key:'operation', label:'Operation', type:'select', options:['list_issues','create_issue','get_issue','update_issue','list_teams','list_projects'] },
11179
+ { key:'token', label:'API Key', type:'password', placeholder:'lin_api_…' },
11180
+ { key:'team_id', label:'Team ID', type:'text', placeholder:'' },
11181
+ { key:'issue_id', label:'Issue ID (get/update)',type:'text', placeholder:'' },
11182
+ { key:'title', label:'Title (create)',type:'text', placeholder:'Fix: something broke' },
11183
+ { key:'description', label:'Description', type:'textarea', placeholder:'Details…' },
11184
+ { key:'priority', label:'Priority (create)',type:'number', placeholder:'1 = urgent, 4 = low' },
11185
+ ],
11186
+ 'service.airtable': [
11187
+ { key:'operation', label:'Operation', type:'select', options:['list_records','create_record','get_record','update_record','delete_record'] },
11188
+ { key:'token', label:'Access Token', type:'password', placeholder:'patXXX…' },
11189
+ { key:'base_id', label:'Base ID', type:'text', placeholder:'appXXX…' },
11190
+ { key:'table', label:'Table Name', type:'text', placeholder:'Tasks' },
11191
+ { key:'record_id', label:'Record ID (get/update/delete)',type:'text', placeholder:'recXXX…' },
11192
+ { key:'filter_formula', label:'Filter Formula (list)',type:'text', placeholder:'AND({Status}="Done")' },
11193
+ { key:'max_records', label:'Max Records (list)',type:'number', placeholder:'100' },
11194
+ { key:'fields', label:'Fields JSON (create/update)',type:'textarea', placeholder:'{"Name":"{{$json.title}}"}' },
11195
+ ],
11196
+ 'service.stripe': [
11197
+ { key:'operation', label:'Operation', type:'select', options:['list_customers','create_customer','get_customer','list_charges','create_charge','list_subscriptions','create_subscription','cancel_subscription','list_products','create_payment_intent'] },
11198
+ { key:'api_key', label:'Secret Key', type:'password', placeholder:'sk_live_…' },
11199
+ { key:'customer_id', label:'Customer ID', type:'text', placeholder:'cus_…' },
11200
+ { key:'limit', label:'Limit', type:'number', placeholder:'20' },
11201
+ { key:'amount', label:'Amount (cents, charge)',type:'number', placeholder:'2000' },
11202
+ { key:'currency', label:'Currency', type:'text', placeholder:'usd' },
11203
+ { key:'email', label:'Email (create customer)',type:'text', placeholder:'customer@example.com' },
11204
+ ],
11205
+ // ── Database ──────────────────────────────────────────────────────────────
11206
+ 'db.postgres': [
11207
+ { key:'host', label:'Host', type:'text', placeholder:'localhost' },
11208
+ { key:'port', label:'Port', type:'number', placeholder:'5432' },
11209
+ { key:'database', label:'Database', type:'text', placeholder:'mydb' },
11210
+ { key:'username', label:'Username', type:'text', placeholder:'postgres' },
11211
+ { key:'password', label:'Password', type:'password', placeholder:'' },
11212
+ { key:'query', label:'SQL', type:'textarea', placeholder:'SELECT * FROM users LIMIT 10' },
11213
+ ],
11214
+ 'db.mysql': [
11215
+ { key:'host', label:'Host', type:'text', placeholder:'localhost' },
11216
+ { key:'port', label:'Port', type:'number', placeholder:'3306' },
11217
+ { key:'database', label:'Database', type:'text', placeholder:'mydb' },
11218
+ { key:'username', label:'Username', type:'text', placeholder:'root' },
11219
+ { key:'password', label:'Password', type:'password', placeholder:'' },
11220
+ { key:'query', label:'SQL', type:'textarea', placeholder:'SELECT * FROM users LIMIT 10' },
11221
+ ],
11222
+ 'db.mongodb': [
11223
+ { key:'uri', label:'Connection URI', type:'password', placeholder:'mongodb://localhost:27017/mydb' },
11224
+ { key:'collection', label:'Collection', type:'text', placeholder:'users' },
11225
+ { key:'operation', label:'Operation', type:'select', options:['find','findOne','insertOne','updateOne','deleteOne'] },
11226
+ { key:'filter', label:'Filter (JSON)', type:'textarea', placeholder:'{"active":true}' },
11227
+ { key:'update', label:'Update (JSON)', type:'textarea', placeholder:'{"$set":{"status":"active"}}' },
11228
+ { key:'limit', label:'Limit', type:'number', placeholder:'100' },
11229
+ ],
11230
+ 'db.redis': [
11231
+ { key:'url', label:'Redis URL', type:'password', placeholder:'redis://localhost:6379' },
11232
+ { key:'operation', label:'Operation', type:'select', options:['get','set','del','keys','hget','hset','lpush','lrange'] },
11233
+ { key:'key', label:'Key', type:'text', placeholder:'mykey' },
11234
+ { key:'value', label:'Value', type:'text', placeholder:'{{$json.value}}' },
11235
+ { key:'ttl', label:'TTL (s)', type:'number', placeholder:'3600' },
11236
+ ],
11237
+ // ── AI ────────────────────────────────────────────────────────────────────
11238
+ 'ai.chat': [
11239
+ { key:'provider_id', label:'Provider', type:'text', placeholder:'anthropic' },
11240
+ { key:'model', label:'Model', type:'text', placeholder:'claude-3-5-sonnet-20241022' },
11241
+ { key:'system_prompt', label:'System Prompt', type:'textarea', placeholder:'You are a helpful assistant.' },
11242
+ { key:'prompt', label:'Prompt', type:'textarea', placeholder:'{{$json.text}}' },
11243
+ { key:'temperature', label:'Temperature', type:'text', placeholder:'0.7' },
11244
+ { key:'max_tokens', label:'Max Tokens', type:'number', placeholder:'1024' },
11245
+ { key:'output_key', label:'Output Key', type:'text', placeholder:'ai_response' },
11246
+ ],
11247
+ 'ai.extract': [
11248
+ { key:'provider_id', label:'Provider', type:'text', placeholder:'anthropic' },
11249
+ { key:'model', label:'Model', type:'text', placeholder:'claude-3-5-haiku-20241022' },
11250
+ { key:'prompt', label:'Extract Prompt', type:'textarea', placeholder:'Extract the name and email from: {{$json.text}}' },
11251
+ { key:'output_schema', label:'Output Schema', type:'textarea', placeholder:'{"name":"string","email":"string"}' },
11252
+ { key:'output_key', label:'Output Key', type:'text', placeholder:'extracted' },
11253
+ ],
11254
+ 'ai.classify': [
11255
+ { key:'provider_id', label:'Provider', type:'text', placeholder:'anthropic' },
11256
+ { key:'model', label:'Model', type:'text', placeholder:'claude-3-5-haiku-20241022' },
11257
+ { key:'categories', label:'Categories', type:'text', placeholder:'positive,negative,neutral' },
11258
+ { key:'prompt_template', label:'Prompt', type:'textarea', placeholder:'Classify this text: {{$json.text}}' },
11259
+ ],
11260
+ 'ai.transform': [
11261
+ { key:'provider_id', label:'Provider', type:'text', placeholder:'anthropic' },
11262
+ { key:'model', label:'Model', type:'text', placeholder:'claude-3-5-sonnet-20241022' },
11263
+ { key:'instruction', label:'Instruction', type:'textarea', placeholder:'Summarize this in 2 sentences' },
11264
+ { key:'input_field', label:'Input Field', type:'text', placeholder:'content' },
11265
+ { key:'output_key', label:'Output Key', type:'text', placeholder:'transformed' },
11266
+ ],
11267
+ 'ai.agent': [
11268
+ { key:'provider_id', label:'Provider', type:'text', placeholder:'anthropic' },
11269
+ { key:'model', label:'Model', type:'text', placeholder:'claude-3-5-sonnet-20241022' },
11270
+ { key:'goal', label:'Goal', type:'textarea', placeholder:'Research and summarize the topic: {{$json.query}}' },
11271
+ { key:'max_steps', label:'Max Steps', type:'number', placeholder:'5' },
11272
+ ],
11273
+ };
11274
+
11275
+ let pbState = {
11276
+ playbook: { id: '', name: 'Untitled', nodes: [], connections: [] },
11277
+ positions: {}, // { nodeId: { x, y } }
11278
+ runStatus: {}, // { nodeId: 'running'|'completed'|'failed' }
11279
+ selectedId: null,
11280
+ connecting: null, // nodeId being connected from
11281
+ _pbInited: false,
11282
+ _dragNode: null,
11283
+ _dragOff: { x: 0, y: 0 },
11284
+ };
11285
+
11286
+ let _pbWs = null;
11287
+ function pbHandleEvent(ev) {
11288
+ if (!ev) return;
11289
+ if (ev.nodeId) {
11290
+ if (ev.eventType === 'step_started') { pbState.runStatus[ev.nodeId] = 'running'; pbUpdateNodeStatus(ev.nodeId); }
11291
+ if (ev.eventType === 'step_completed') { pbState.runStatus[ev.nodeId] = 'completed'; pbUpdateNodeStatus(ev.nodeId); }
11292
+ if (ev.eventType === 'step_failed') { pbState.runStatus[ev.nodeId] = 'failed'; pbUpdateNodeStatus(ev.nodeId); }
11293
+ }
11294
+ if (ev.eventType === 'run_started') { pbState.runStatus = {}; pbRenderCanvas(); }
11295
+ if (ev.eventType === 'run_completed' || ev.eventType === 'run_failed' || ev.eventType === 'run_stopped') {
11296
+ const lbl = document.getElementById('pb-run-status');
11297
+ if (lbl) lbl.textContent = '● ' + ev.eventType.replace('run_', '').toUpperCase();
11298
+ setTimeout(() => loadWorkflowRuns(), 1000);
11299
+ }
11300
+ }
11301
+
11302
+ function pbInit() {
11303
+ if (pbState._pbInited) { pbRenderCanvas(); return; }
11304
+ pbState._pbInited = true;
11305
+ pbRenderPalette();
11306
+ pbRenderCanvas();
11307
+ pbConnectEvents();
11308
+ }
11309
+
11310
+ function pbConnectEvents() {
11311
+ // Connect to the monobrowse dashboard WS/SSE on port 4243 for live step events
11312
+ if (_pbWs) { try { _pbWs.close(); } catch {} _pbWs = null; }
11313
+ try {
11314
+ const ws = new WebSocket('ws://localhost:4243');
11315
+ ws.onmessage = (e) => { try { const d = JSON.parse(e.data); if (d.type === 'history') return; pbHandleEvent(d); } catch {} };
11316
+ ws.onerror = () => pbConnectSse();
11317
+ ws.onclose = () => { _pbWs = null; };
11318
+ _pbWs = ws;
11319
+ } catch { pbConnectSse(); }
11320
+ }
11321
+
11322
+ function pbConnectSse() {
11323
+ try {
11324
+ const es = new EventSource('http://localhost:4243/events');
11325
+ es.onmessage = (e) => { try { pbHandleEvent(JSON.parse(e.data)); } catch {} };
11326
+ es.onerror = () => es.close();
11327
+ } catch {}
11328
+ }
11329
+
11330
+ function pbRenderPalette() {
11331
+ const el = document.getElementById('pb-palette');
11332
+ if (!el) return;
11333
+ const cats = [...new Set(PB_NODE_DEFS.map(n => n.cat))];
11334
+ el.innerHTML = cats.map(cat => {
11335
+ const nodes = PB_NODE_DEFS.filter(n => n.cat === cat);
11336
+ const catColor = PB_CAT_COLOR[cat] || '#6b7280';
11337
+ return `<div>
11338
+ <div class="pb-cat-hdr" onclick="pbToggleCat(this)">
11339
+ <span class="pb-cat-arrow">▸</span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${catColor};margin-right:5px;vertical-align:middle;flex-shrink:0"></span>${cat}
11340
+ </div>
11341
+ <div class="pb-cat-body ${cat === cats[0] ? '' : 'collapsed'}">
11342
+ ${nodes.map((n,i) => `<button class="pb-node-btn" onclick="pbAddNode('${esc(n.type)}',PB_NODE_DEFS.filter(d=>d.cat==='${cat}')[${i}])" title="${esc(n.type)}${n.defaultConfig ? ' (op: '+n.defaultConfig.operation+')' : ''}">
11343
+ <span class="pb-node-icon">${n.icon}</span>${esc(n.label)}
11344
+ </button>`).join('')}
11345
+ </div>
11346
+ </div>`;
11347
+ }).join('');
11348
+ // Expand first category by default
11349
+ el.querySelector('.pb-cat-hdr')?.classList.add('expanded');
11350
+ const firstArrow = el.querySelector('.pb-cat-arrow');
11351
+ if (firstArrow) firstArrow.textContent = '▾';
11352
+ }
11353
+
11354
+ function pbToggleCat(hdr) {
11355
+ const body = hdr.nextElementSibling;
11356
+ const arrow = hdr.querySelector('.pb-cat-arrow');
11357
+ body.classList.toggle('collapsed');
11358
+ if (arrow) arrow.textContent = body.classList.contains('collapsed') ? '▸' : '▾';
11359
+ }
11360
+
11361
+ function pbNew() {
11362
+ pbState.playbook = { id: 'pb-' + Date.now(), name: 'Untitled', nodes: [], connections: [] };
11363
+ pbState.positions = {};
11364
+ pbState.runStatus = {};
11365
+ pbState.selectedId = null;
11366
+ document.getElementById('pb-name-input').value = 'Untitled';
11367
+ document.getElementById('pb-run-status').textContent = '';
11368
+ pbRenderCanvas();
11369
+ pbShowConfig(null);
11370
+ }
11371
+
11372
+ function pbAddNode(type, defOverride) {
11373
+ // defOverride lets palette pass the specific def when multiple defs share a type (e.g. service.gmail)
11374
+ const def = defOverride || PB_NODE_DEFS.find(n => n.type === type) || { label: type, icon: '⊹' };
11375
+ const id = 'n-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
11376
+ const pb = pbState.playbook;
11377
+ // Offset new nodes so they don't stack
11378
+ const offset = pb.nodes.length * 20;
11379
+ pbState.positions[id] = { x: 60 + offset, y: 60 + offset };
11380
+ pb.nodes.push({ id, type, name: def.label, config: Object.assign({}, def.defaultConfig || {}) });
11381
+ const empty = document.getElementById('pb-empty');
11382
+ if (empty) empty.style.display = 'none';
11383
+ pbRenderCanvas();
11384
+ pbSelectNode(id);
11385
+ }
11386
+
11387
+ function pbRenderCanvas() {
11388
+ const canvas = document.getElementById('pb-canvas');
11389
+ const svg = document.getElementById('pb-svg');
11390
+ if (!canvas || !svg) return;
11391
+
11392
+ // Remove old node cards
11393
+ canvas.querySelectorAll('.pb-node-card').forEach(el => el.remove());
11394
+
11395
+ const pb = pbState.playbook;
11396
+ if (!pb.nodes.length) {
11397
+ const empty = document.getElementById('pb-empty');
11398
+ if (empty) empty.style.display = '';
11399
+ svg.innerHTML = '<defs><marker id="pb-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="var(--text-xs)"/></marker></defs>';
11400
+ return;
11401
+ }
11402
+ const empty = document.getElementById('pb-empty');
11403
+ if (empty) empty.style.display = 'none';
11404
+
11405
+ // Render nodes
11406
+ for (const node of pb.nodes) {
11407
+ const pos = pbState.positions[node.id] || { x: 40, y: 40 };
11408
+ const def = PB_NODE_DEFS.find(n => n.type === node.type) || { icon: '⊹' };
11409
+ const st = pbState.runStatus[node.id] || '';
11410
+ const card = document.createElement('div');
11411
+ card.className = 'pb-node-card' + (node.id === pbState.selectedId ? ' selected' : '') + (st ? ' ns-' + st : '');
11412
+ card.dataset.id = node.id;
11413
+ card.style.left = pos.x + 'px';
11414
+ card.style.top = pos.y + 'px';
11415
+ card.innerHTML = `
11416
+ <div class="pb-node-hdr">
11417
+ <span class="pb-node-type-icon">${def.icon}</span>
11418
+ <span class="pb-node-title" title="${esc(node.type)}">${esc(node.name || node.type)}</span>
11419
+ <button class="pb-node-del" onclick="pbDeleteNode('${esc(node.id)}')" title="Remove">✕</button>
11420
+ </div>
11421
+ <div class="pb-node-body">
11422
+ ${st ? `<div class="pb-node-status"><div class="pb-ns-dot ${st}"></div><span style="color:${st==='running'?'var(--accent)':st==='completed'?'var(--green)':'var(--red)'}">${st}</span></div>` : ''}
11423
+ <div style="font-size:10px;color:var(--text-xs)">${esc(node.type)}</div>
11424
+ </div>
11425
+ <button class="pb-node-conn-btn" title="Connect to another node" onclick="pbStartConnect('${esc(node.id)}',event)">→</button>
11426
+ `;
11427
+ card.addEventListener('mousedown', (e) => pbDragStart(node.id, e));
11428
+ card.addEventListener('click', (e) => { if (!e.defaultPrevented) pbSelectNode(node.id); });
11429
+ canvas.appendChild(card);
11430
+ }
11431
+
11432
+ // Render SVG connections
11433
+ const w = canvas.offsetWidth || 800;
11434
+ const h = canvas.offsetHeight || 600;
11435
+ svg.setAttribute('width', w);
11436
+ svg.setAttribute('height', h);
11437
+ let paths = '<defs><marker id="pb-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="var(--text-xs)"/></marker></defs>';
11438
+ for (const conn of pb.connections) {
11439
+ const fromPos = pbState.positions[conn.from];
11440
+ const toPos = pbState.positions[conn.to];
11441
+ if (!fromPos || !toPos) continue;
11442
+ const x1 = fromPos.x + 160; const y1 = fromPos.y + 36;
11443
+ const x2 = toPos.x; const y2 = toPos.y + 36;
11444
+ const cx1 = x1 + 60; const cx2 = x2 - 60;
11445
+ const label = conn.handle ? `<text x="${(x1+x2)/2}" y="${(y1+y2)/2 - 5}" font-size="9" fill="var(--text-xs)" text-anchor="middle">${esc(conn.handle)}</text>` : '';
11446
+ paths += `<path d="M${x1},${y1} C${cx1},${y1} ${cx2},${y2} ${x2},${y2}" stroke="var(--border)" stroke-width="2" fill="none" marker-end="url(#pb-arrow)"/>${label}`;
11447
+ }
11448
+ svg.innerHTML = paths;
11449
+ }
11450
+
11451
+ function pbDragStart(nodeId, e) {
11452
+ if (e.target.classList.contains('pb-node-del') || e.target.classList.contains('pb-node-conn-btn')) return;
11453
+ e.preventDefault();
11454
+ const pos = pbState.positions[nodeId] || { x: 0, y: 0 };
11455
+ pbState._dragNode = nodeId;
11456
+ pbState._dragOff = { x: e.clientX - pos.x, y: e.clientY - pos.y };
11457
+ function onMove(ev) {
11458
+ const wrap = document.getElementById('pb-canvas-wrap');
11459
+ const rect = wrap ? wrap.getBoundingClientRect() : { left: 0, top: 0 };
11460
+ pbState.positions[nodeId] = { x: Math.max(0, ev.clientX - pbState._dragOff.x), y: Math.max(0, ev.clientY - pbState._dragOff.y) };
11461
+ const card = document.querySelector('.pb-node-card[data-id="' + nodeId + '"]');
11462
+ if (card) { card.style.left = pbState.positions[nodeId].x + 'px'; card.style.top = pbState.positions[nodeId].y + 'px'; }
11463
+ pbRenderSvg();
11464
+ }
11465
+ function onUp() {
11466
+ pbState._dragNode = null;
11467
+ document.removeEventListener('mousemove', onMove);
11468
+ document.removeEventListener('mouseup', onUp);
11469
+ }
11470
+ document.addEventListener('mousemove', onMove);
11471
+ document.addEventListener('mouseup', onUp);
11472
+ }
11473
+
11474
+ function pbRenderSvg() {
11475
+ const svg = document.getElementById('pb-svg');
11476
+ const canvas = document.getElementById('pb-canvas');
11477
+ if (!svg || !canvas) return;
11478
+ const w = canvas.offsetWidth || 800;
11479
+ const h = canvas.offsetHeight || 600;
11480
+ svg.setAttribute('width', w);
11481
+ svg.setAttribute('height', h);
11482
+ let paths = '<defs><marker id="pb-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="var(--text-xs)"/></marker></defs>';
11483
+ for (const conn of pbState.playbook.connections) {
11484
+ const fromPos = pbState.positions[conn.from];
11485
+ const toPos = pbState.positions[conn.to];
11486
+ if (!fromPos || !toPos) continue;
11487
+ const x1 = fromPos.x + 160; const y1 = fromPos.y + 36;
11488
+ const x2 = toPos.x; const y2 = toPos.y + 36;
11489
+ const cx1 = x1 + 60; const cx2 = x2 - 60;
11490
+ const label = conn.handle ? `<text x="${(x1+x2)/2}" y="${(y1+y2)/2 - 5}" font-size="9" fill="var(--text-xs)" text-anchor="middle">${esc(conn.handle)}</text>` : '';
11491
+ paths += `<path d="M${x1},${y1} C${cx1},${y1} ${cx2},${y2} ${x2},${y2}" stroke="var(--border)" stroke-width="2" fill="none" marker-end="url(#pb-arrow)"/>${label}`;
11492
+ }
11493
+ svg.innerHTML = paths;
11494
+ }
11495
+
11496
+ function pbStartConnect(fromId, e) {
11497
+ e.stopPropagation();
11498
+ e.preventDefault();
11499
+ if (pbState.connecting) {
11500
+ // Second click: connect from→to
11501
+ const toId = pbState.connecting === fromId ? null : fromId;
11502
+ if (toId) pbAddConnection(pbState._connectFrom, toId);
11503
+ pbState.connecting = null;
11504
+ pbState._connectFrom = null;
11505
+ document.querySelectorAll('.pb-node-card').forEach(c => c.style.outline = '');
11506
+ } else {
11507
+ pbState.connecting = fromId;
11508
+ pbState._connectFrom = fromId;
11509
+ // Highlight the from node
11510
+ const card = document.querySelector('.pb-node-card[data-id="' + fromId + '"]');
11511
+ if (card) card.style.outline = '2px solid var(--accent)';
11512
+ // Next click on any node's conn-btn will complete
11513
+ showToast('Connect mode', 'Click → on target node to connect', 'info');
11514
+ }
11515
+ }
11516
+
11517
+ function pbAddConnection(fromId, toId) {
11518
+ const handle = prompt('Handle (optional, e.g. true/false for if-node, or case value for switch):') || undefined;
11519
+ pbState.playbook.connections.push({ from: fromId, to: toId, handle: handle || undefined });
11520
+ pbRenderCanvas();
11521
+ }
11522
+
11523
+ function pbSelectNode(nodeId) {
11524
+ pbState.selectedId = nodeId;
11525
+ document.querySelectorAll('.pb-node-card').forEach(c => c.classList.toggle('selected', c.dataset.id === nodeId));
11526
+ pbShowConfig(nodeId);
11527
+ }
11528
+
11529
+ function pbShowConfig(nodeId) {
11530
+ const title = document.getElementById('pb-config-title');
11531
+ const body = document.getElementById('pb-config-body');
11532
+ if (!nodeId) {
11533
+ if (title) title.textContent = 'Select a node';
11534
+ if (body) body.innerHTML = '<div style="font-size:11px;color:var(--text-xs);margin-top:8px">Click a node on the canvas to configure it.</div>';
11535
+ return;
11536
+ }
11537
+ const node = pbState.playbook.nodes.find(n => n.id === nodeId);
11538
+ if (!node) return;
11539
+ const fields = PB_NODE_FIELDS[node.type] || [];
11540
+ if (title) title.textContent = node.name || node.type;
11541
+ if (!body) return;
11542
+ body.innerHTML = `
11543
+ <div class="pb-cfg-row">
11544
+ <div class="pb-cfg-lbl">Node Name</div>
11545
+ <input class="pb-cfg-inp" type="text" value="${esc(node.name || '')}" oninput="pbUpdateNodeName('${nodeId}',this.value)">
11546
+ </div>
11547
+ ${fields.map(f => {
11548
+ const val = node.config[f.key] !== undefined ? node.config[f.key] : '';
11549
+ if (f.type === 'select') {
11550
+ return `<div class="pb-cfg-row"><div class="pb-cfg-lbl">${esc(f.label)}</div>
11551
+ <select class="pb-cfg-inp" onchange="pbUpdateConfig('${nodeId}','${f.key}',this.value)">
11552
+ ${(f.options || []).map(o => `<option value="${esc(o)}" ${val === o ? 'selected' : ''}>${esc(o)}</option>`).join('')}
11553
+ </select></div>`;
11554
+ }
11555
+ if (f.type === 'textarea') {
11556
+ return `<div class="pb-cfg-row"><div class="pb-cfg-lbl">${esc(f.label)}</div>
11557
+ <textarea class="pb-cfg-inp" rows="4" placeholder="${esc(f.placeholder||'')}" oninput="pbUpdateConfig('${nodeId}','${f.key}',this.value)">${esc(String(val))}</textarea>${f.help ? `<div style="font-size:10px;color:var(--text-xs);margin-top:2px">${esc(f.help)}</div>` : ''}</div>`;
11558
+ }
11559
+ if (f.type === 'password') {
11560
+ return `<div class="pb-cfg-row"><div class="pb-cfg-lbl">${esc(f.label)}</div>
11561
+ <input class="pb-cfg-inp" type="password" value="${esc(String(val))}" placeholder="${esc(f.placeholder||'')}" oninput="pbUpdateConfig('${nodeId}','${f.key}',this.value)">${f.help ? `<div style="font-size:10px;color:var(--text-xs);margin-top:2px">${esc(f.help)}</div>` : ''}</div>`;
11562
+ }
11563
+ return `<div class="pb-cfg-row"><div class="pb-cfg-lbl">${esc(f.label)}</div>
11564
+ <input class="pb-cfg-inp" type="${f.type === 'number' ? 'number' : 'text'}" value="${esc(String(val))}" placeholder="${esc(f.placeholder||'')}" oninput="pbUpdateConfig('${nodeId}','${f.key}',this.value)">${f.help ? `<div style="font-size:10px;color:var(--text-xs);margin-top:2px">${esc(f.help)}</div>` : ''}</div>`;
11565
+ }).join('')}
11566
+ <div style="margin-top:14px;padding-top:10px;border-top:1px solid var(--border)">
11567
+ <div class="pb-cfg-lbl">Connections from this node</div>
11568
+ ${pbState.playbook.connections.filter(c=>c.from===nodeId).map((c,i) => `
11569
+ <div style="display:flex;align-items:center;gap:6px;margin-top:4px;font-size:11px;color:var(--text-lo)">
11570
+ <span>→ ${esc(c.to.slice(0,10))}…${c.handle ? ' <em>['+esc(c.handle)+']</em>' : ''}</span>
11571
+ <button class="btn" style="font-size:10px;padding:1px 6px" onclick="pbRemoveConn(${i})">✕</button>
11572
+ </div>`).join('') || '<div style="font-size:11px;color:var(--text-xs);margin-top:4px">None yet. Use → button on canvas.</div>'}
11573
+ </div>
11574
+ `;
11575
+ }
11576
+
11577
+ function pbUpdateNodeName(id, name) {
11578
+ const node = pbState.playbook.nodes.find(n => n.id === id);
11579
+ if (node) { node.name = name; pbRenderCanvas(); }
11580
+ }
11581
+
11582
+ function pbUpdateConfig(id, key, value) {
11583
+ const node = pbState.playbook.nodes.find(n => n.id === id);
11584
+ if (node) node.config[key] = value;
11585
+ }
11586
+
11587
+ function pbDeleteNode(id) {
11588
+ pbState.playbook.nodes = pbState.playbook.nodes.filter(n => n.id !== id);
11589
+ pbState.playbook.connections = pbState.playbook.connections.filter(c => c.from !== id && c.to !== id);
11590
+ delete pbState.positions[id];
11591
+ delete pbState.runStatus[id];
11592
+ if (pbState.selectedId === id) { pbState.selectedId = null; pbShowConfig(null); }
11593
+ pbRenderCanvas();
11594
+ }
11595
+
11596
+ function pbRemoveConn(idx) {
11597
+ pbState.playbook.connections.splice(idx, 1);
11598
+ pbShowConfig(pbState.selectedId);
11599
+ pbRenderCanvas();
11600
+ }
11601
+
11602
+ function pbUpdateNodeStatus(nodeId) {
11603
+ const st = pbState.runStatus[nodeId] || '';
11604
+ const card = document.querySelector('.pb-node-card[data-id="' + nodeId + '"]');
11605
+ if (!card) return;
11606
+ card.className = 'pb-node-card' + (nodeId === pbState.selectedId ? ' selected' : '') + (st ? ' ns-' + st : '');
11607
+ const body = card.querySelector('.pb-node-body');
11608
+ if (body) body.innerHTML = `
11609
+ ${st ? `<div class="pb-node-status"><div class="pb-ns-dot ${st}"></div><span style="color:${st==='running'?'var(--accent)':st==='completed'?'var(--green)':'var(--red)'}">${st}</span></div>` : ''}
11610
+ <div style="font-size:10px;color:var(--text-xs)">${esc(pbState.playbook.nodes.find(n=>n.id===nodeId)?.type||'')}</div>
11611
+ `;
11612
+ }
11613
+
11614
+ async function pbSave() {
11615
+ const pb = pbState.playbook;
11616
+ if (!pb.name || pb.name === 'Untitled') {
11617
+ pb.name = document.getElementById('pb-name-input').value || 'Untitled';
11618
+ }
11619
+ if (!pb.id) pb.id = 'pb-' + Date.now();
11620
+ // Save to .monomind/playbooks/<id>.json via API
11621
+ try {
11622
+ const resp = await fetch('/api/playbooks', {
11623
+ method: 'POST',
11624
+ headers: { 'Content-Type': 'application/json' },
11625
+ body: JSON.stringify(pb),
11626
+ });
11627
+ if (resp.ok) {
11628
+ showToast('Saved', 'Playbook saved to .monomind/playbooks/', 'ok');
11629
+ loadWorkflowDefs();
11630
+ } else {
11631
+ const txt = await resp.text();
11632
+ showToast('Save failed', txt, 'err');
11633
+ }
11634
+ } catch (e) {
11635
+ showToast('Save error', e.message, 'err');
11636
+ }
11637
+ }
11638
+
11639
+ async function pbLoad() {
11640
+ try {
11641
+ _wfDefs = await apiFetch('/api/workflow-defs');
11642
+ } catch { _wfDefs = []; }
11643
+ if (!_wfDefs.length) { showToast('No playbooks', 'No saved playbooks found', 'warn'); return; }
11644
+ const names = _wfDefs.map((d, i) => i + ': ' + (d.name || d.id)).join('\n');
11645
+ const choice = prompt('Select a playbook to load:\n' + names);
11646
+ const idx = parseInt(choice, 10);
11647
+ if (isNaN(idx) || !_wfDefs[idx]) return;
11648
+ pbLoadDef(_wfDefs[idx].id);
11649
+ }
11650
+
11651
+ async function pbLoadDef(playbookId) {
11652
+ try {
11653
+ // First try to read the saved JSON file directly via workflow-defs + file read
11654
+ // Try monobrowse API for full definition (includes node config)
11655
+ let pb = null;
11656
+ try {
11657
+ pb = await apiFetch('http://localhost:4243/api/playbooks/' + encodeURIComponent(playbookId));
11658
+ } catch {
11659
+ // Fall back: read the .json file via the defs list and reconstruct
11660
+ const defs = await apiFetch('/api/workflow-defs').catch(() => []);
11661
+ const def = defs.find(d => d.id === playbookId);
11662
+ if (!def) { showToast('Not found', 'Playbook not found: ' + playbookId, 'err'); return; }
11663
+ pb = { id: def.id, name: def.name, description: def.description || '', nodes: [], connections: [] };
11664
+ }
11665
+ pbState.playbook = pb;
11666
+ pbState.positions = {};
11667
+ pbState.runStatus = {};
11668
+ pbState.selectedId = null;
11669
+ pb.nodes.forEach((n, i) => {
11670
+ pbState.positions[n.id] = { x: 60 + (i % 4) * 200, y: 60 + Math.floor(i / 4) * 120 };
11671
+ });
11672
+ document.getElementById('pb-name-input').value = pb.name || '';
11673
+ document.getElementById('pb-run-status').textContent = '';
11674
+ pbRenderCanvas();
11675
+ pbShowConfig(null);
11676
+ showToast('Loaded', pb.name || playbookId, 'ok');
11677
+ } catch (e) {
11678
+ showToast('Load error', e.message, 'err');
11679
+ }
11680
+ }
11681
+
11682
+ function pbImportJSON() {
11683
+ const input = document.createElement('input');
11684
+ input.type = 'file';
11685
+ input.accept = '.json';
11686
+ input.onchange = () => {
11687
+ const file = input.files[0];
11688
+ if (!file) return;
11689
+ const reader = new FileReader();
11690
+ reader.onload = (ev) => {
11691
+ try {
11692
+ const pb = JSON.parse(ev.target.result);
11693
+ if (!pb.id || !pb.nodes) { showToast('Invalid JSON', 'Missing id or nodes', 'err'); return; }
11694
+ pbState.playbook = pb;
11695
+ pbState.positions = {};
11696
+ pbState.runStatus = {};
11697
+ pbState.selectedId = null;
11698
+ pb.nodes.forEach((n, i) => { pbState.positions[n.id] = { x: 60 + (i % 4) * 200, y: 60 + Math.floor(i / 4) * 120 }; });
11699
+ document.getElementById('pb-name-input').value = pb.name || '';
11700
+ pbRenderCanvas();
11701
+ showToast('Imported', pb.name || pb.id, 'ok');
11702
+ } catch { showToast('Parse error', 'Invalid JSON file', 'err'); }
11703
+ };
11704
+ reader.readAsText(file);
11705
+ };
11706
+ input.click();
11707
+ }
11708
+
11709
+ function pbExportJSON() {
11710
+ const pb = pbState.playbook;
11711
+ if (!pb.nodes.length) { showToast('Nothing to export', 'Add nodes first', 'warn'); return; }
11712
+ const json = JSON.stringify(pb, null, 2);
11713
+ const blob = new Blob([json], { type: 'application/json' });
11714
+ const url = URL.createObjectURL(blob);
11715
+ const a = document.createElement('a');
11716
+ a.href = url;
11717
+ a.download = (pb.id || 'playbook') + '.json';
11718
+ a.click();
11719
+ URL.revokeObjectURL(url);
11720
+ }
11721
+
11722
+ async function pbRun() {
11723
+ const pb = pbState.playbook;
11724
+ if (!pb.nodes.length) { showToast('Empty playbook', 'Add nodes first', 'warn'); return; }
11725
+ if (!pb.id) pb.id = 'pb-' + Date.now();
11726
+ await pbSave();
11727
+ pbState.runStatus = {};
11728
+ document.getElementById('pb-run-status').textContent = '● STARTING…';
11729
+ try {
11730
+ const resp = await fetch('http://localhost:4243/api/playbooks/' + encodeURIComponent(pb.id) + '/run', {
11731
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
11732
+ });
11733
+ if (resp.ok) {
11734
+ showToast('Running', pb.name || pb.id, 'ok');
11735
+ } else {
11736
+ const txt = await resp.text();
11737
+ showToast('Run failed', txt, 'err');
11738
+ document.getElementById('pb-run-status').textContent = '✗ FAILED';
11739
+ }
11740
+ } catch {
11741
+ showToast('Offline', 'Start monobrowse: npx monomind browse dashboard', 'warn');
11742
+ document.getElementById('pb-run-status').textContent = '✗ OFFLINE';
11743
+ }
11744
+ }
11745
+
11746
+ /* ═══════════════════════════════════════════════════════════════ */
11747
+
10110
11748
  async function loadMonograph() {
10111
11749
  if (_mgLoaded) return;
10112
11750
  const loadDir = DIR; // snapshot DIR at call time
@@ -12191,9 +13829,10 @@ function mmRenderOrgs(body) {
12191
13829
  if (!orgs.length) { body.innerHTML = '<div class="empty">No orgs found. Run /mastermind:createorg to create one.</div>'; return; }
12192
13830
  body.innerHTML = orgs.map(o => {
12193
13831
  const running = o.running;
13832
+ const _liveLabel = running ? (o.lastEventAt ? (Date.now()-o.lastEventAt<1800000 ? '🟢 LIVE' : Date.now()-o.lastEventAt<7200000 ? '🟡 QUIET' : '🔴 STALE') : '🟡 QUIET') : '';
12194
13833
  return `<div class="mm-skill-item" data-org="${esc(o.name)}" onclick="closeMastermind();v2SelectOrg(this.dataset.org);switchView('orgs')">
12195
13834
  <span class="mm-skill-name">${esc(o.name)}</span>
12196
- <span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${running ? '⬤ LIVE' : ''}</span>
13835
+ <span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${_liveLabel}</span>
12197
13836
  </div>`;
12198
13837
  }).join('');
12199
13838
  }