botmux 2.33.0 → 2.34.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 (281) hide show
  1. package/README.en.md +12 -1
  2. package/README.md +45 -1
  3. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/cli/claude-code.js +11 -0
  5. package/dist/adapters/cli/claude-code.js.map +1 -1
  6. package/dist/cli/bots-list-output.d.ts +21 -0
  7. package/dist/cli/bots-list-output.d.ts.map +1 -0
  8. package/dist/cli/bots-list-output.js +23 -0
  9. package/dist/cli/bots-list-output.js.map +1 -0
  10. package/dist/cli/workflow.d.ts +13 -0
  11. package/dist/cli/workflow.d.ts.map +1 -0
  12. package/dist/cli/workflow.js +781 -0
  13. package/dist/cli/workflow.js.map +1 -0
  14. package/dist/cli.js +69 -14
  15. package/dist/cli.js.map +1 -1
  16. package/dist/core/command-handler.d.ts.map +1 -1
  17. package/dist/core/command-handler.js +219 -6
  18. package/dist/core/command-handler.js.map +1 -1
  19. package/dist/core/session-manager.d.ts +6 -1
  20. package/dist/core/session-manager.d.ts.map +1 -1
  21. package/dist/core/session-manager.js +22 -12
  22. package/dist/core/session-manager.js.map +1 -1
  23. package/dist/core/worker-pool.d.ts +13 -0
  24. package/dist/core/worker-pool.d.ts.map +1 -1
  25. package/dist/core/worker-pool.js +100 -6
  26. package/dist/core/worker-pool.js.map +1 -1
  27. package/dist/daemon.d.ts +3 -0
  28. package/dist/daemon.d.ts.map +1 -1
  29. package/dist/daemon.js +884 -3
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/dashboard/auth.d.ts +36 -0
  32. package/dist/dashboard/auth.d.ts.map +1 -1
  33. package/dist/dashboard/auth.js +22 -0
  34. package/dist/dashboard/auth.js.map +1 -1
  35. package/dist/dashboard/web/app.js +20 -1
  36. package/dist/dashboard/web/app.js.map +1 -1
  37. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  38. package/dist/dashboard/web/i18n.js +356 -0
  39. package/dist/dashboard/web/i18n.js.map +1 -1
  40. package/dist/dashboard/web/workflow-catalog.d.ts +2 -0
  41. package/dist/dashboard/web/workflow-catalog.d.ts.map +1 -0
  42. package/dist/dashboard/web/workflow-catalog.js +323 -0
  43. package/dist/dashboard/web/workflow-catalog.js.map +1 -0
  44. package/dist/dashboard/web/workflows.d.ts +2 -0
  45. package/dist/dashboard/web/workflows.d.ts.map +1 -0
  46. package/dist/dashboard/web/workflows.js +1618 -0
  47. package/dist/dashboard/web/workflows.js.map +1 -0
  48. package/dist/dashboard/workflow-api.d.ts +23 -0
  49. package/dist/dashboard/workflow-api.d.ts.map +1 -0
  50. package/dist/dashboard/workflow-api.js +463 -0
  51. package/dist/dashboard/workflow-api.js.map +1 -0
  52. package/dist/dashboard-web/app.js +494 -199
  53. package/dist/dashboard-web/index.html +1 -0
  54. package/dist/dashboard-web/style.css +160 -6
  55. package/dist/dashboard-web/terminal-replay.html +227 -0
  56. package/dist/dashboard.js +29 -12
  57. package/dist/dashboard.js.map +1 -1
  58. package/dist/i18n/en.d.ts.map +1 -1
  59. package/dist/i18n/en.js +12 -0
  60. package/dist/i18n/en.js.map +1 -1
  61. package/dist/i18n/zh.d.ts.map +1 -1
  62. package/dist/i18n/zh.js +12 -0
  63. package/dist/i18n/zh.js.map +1 -1
  64. package/dist/im/lark/card-handler.d.ts +3 -0
  65. package/dist/im/lark/card-handler.d.ts.map +1 -1
  66. package/dist/im/lark/card-handler.js +27 -1
  67. package/dist/im/lark/card-handler.js.map +1 -1
  68. package/dist/im/lark/client.d.ts +19 -2
  69. package/dist/im/lark/client.d.ts.map +1 -1
  70. package/dist/im/lark/client.js +21 -2
  71. package/dist/im/lark/client.js.map +1 -1
  72. package/dist/im/lark/workflow-card-handler.d.ts +50 -0
  73. package/dist/im/lark/workflow-card-handler.d.ts.map +1 -0
  74. package/dist/im/lark/workflow-card-handler.js +152 -0
  75. package/dist/im/lark/workflow-card-handler.js.map +1 -0
  76. package/dist/im/lark/workflow-cards.d.ts +46 -0
  77. package/dist/im/lark/workflow-cards.d.ts.map +1 -0
  78. package/dist/im/lark/workflow-cards.js +226 -0
  79. package/dist/im/lark/workflow-cards.js.map +1 -0
  80. package/dist/im/lark/workflow-progress-card.d.ts +76 -0
  81. package/dist/im/lark/workflow-progress-card.d.ts.map +1 -0
  82. package/dist/im/lark/workflow-progress-card.js +279 -0
  83. package/dist/im/lark/workflow-progress-card.js.map +1 -0
  84. package/dist/im/lark/workflow-slash-command.d.ts +92 -0
  85. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -0
  86. package/dist/im/lark/workflow-slash-command.js +185 -0
  87. package/dist/im/lark/workflow-slash-command.js.map +1 -0
  88. package/dist/services/group-creator.d.ts.map +1 -1
  89. package/dist/services/group-creator.js +17 -4
  90. package/dist/services/group-creator.js.map +1 -1
  91. package/dist/services/groups-store.d.ts +11 -0
  92. package/dist/services/groups-store.d.ts.map +1 -1
  93. package/dist/services/groups-store.js +26 -0
  94. package/dist/services/groups-store.js.map +1 -1
  95. package/dist/services/jsonl-cursor.d.ts +12 -0
  96. package/dist/services/jsonl-cursor.d.ts.map +1 -0
  97. package/dist/services/jsonl-cursor.js +45 -0
  98. package/dist/services/jsonl-cursor.js.map +1 -0
  99. package/dist/services/schedule-store.d.ts +35 -0
  100. package/dist/services/schedule-store.d.ts.map +1 -1
  101. package/dist/services/schedule-store.js +108 -1
  102. package/dist/services/schedule-store.js.map +1 -1
  103. package/dist/skills/definitions.d.ts.map +1 -1
  104. package/dist/skills/definitions.js +399 -0
  105. package/dist/skills/definitions.js.map +1 -1
  106. package/dist/types.d.ts +4 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  109. package/dist/utils/cli-usage-limit.js +4 -0
  110. package/dist/utils/cli-usage-limit.js.map +1 -1
  111. package/dist/worker.js +118 -14
  112. package/dist/worker.js.map +1 -1
  113. package/dist/workflows/attempt-resume.d.ts +114 -0
  114. package/dist/workflows/attempt-resume.d.ts.map +1 -0
  115. package/dist/workflows/attempt-resume.js +385 -0
  116. package/dist/workflows/attempt-resume.js.map +1 -0
  117. package/dist/workflows/attempt-terminal.d.ts +21 -0
  118. package/dist/workflows/attempt-terminal.d.ts.map +1 -0
  119. package/dist/workflows/attempt-terminal.js +7 -0
  120. package/dist/workflows/attempt-terminal.js.map +1 -0
  121. package/dist/workflows/blob.d.ts +27 -0
  122. package/dist/workflows/blob.d.ts.map +1 -0
  123. package/dist/workflows/blob.js +39 -0
  124. package/dist/workflows/blob.js.map +1 -0
  125. package/dist/workflows/cancel-run.d.ts +45 -0
  126. package/dist/workflows/cancel-run.d.ts.map +1 -0
  127. package/dist/workflows/cancel-run.js +99 -0
  128. package/dist/workflows/cancel-run.js.map +1 -0
  129. package/dist/workflows/cancel.d.ts +111 -0
  130. package/dist/workflows/cancel.d.ts.map +1 -0
  131. package/dist/workflows/cancel.js +120 -0
  132. package/dist/workflows/cancel.js.map +1 -0
  133. package/dist/workflows/catalog.d.ts +60 -0
  134. package/dist/workflows/catalog.d.ts.map +1 -0
  135. package/dist/workflows/catalog.js +119 -0
  136. package/dist/workflows/catalog.js.map +1 -0
  137. package/dist/workflows/cold-attach.d.ts +30 -0
  138. package/dist/workflows/cold-attach.d.ts.map +1 -0
  139. package/dist/workflows/cold-attach.js +40 -0
  140. package/dist/workflows/cold-attach.js.map +1 -0
  141. package/dist/workflows/cold-scan.d.ts +21 -0
  142. package/dist/workflows/cold-scan.d.ts.map +1 -0
  143. package/dist/workflows/cold-scan.js +70 -0
  144. package/dist/workflows/cold-scan.js.map +1 -0
  145. package/dist/workflows/daemon-spawn.d.ts +117 -0
  146. package/dist/workflows/daemon-spawn.d.ts.map +1 -0
  147. package/dist/workflows/daemon-spawn.js +551 -0
  148. package/dist/workflows/daemon-spawn.js.map +1 -0
  149. package/dist/workflows/definition.d.ts +1309 -0
  150. package/dist/workflows/definition.d.ts.map +1 -0
  151. package/dist/workflows/definition.js +334 -0
  152. package/dist/workflows/definition.js.map +1 -0
  153. package/dist/workflows/effect-input.d.ts +4 -0
  154. package/dist/workflows/effect-input.d.ts.map +1 -0
  155. package/dist/workflows/effect-input.js +18 -0
  156. package/dist/workflows/effect-input.js.map +1 -0
  157. package/dist/workflows/events/append.d.ts +77 -0
  158. package/dist/workflows/events/append.d.ts.map +1 -0
  159. package/dist/workflows/events/append.js +214 -0
  160. package/dist/workflows/events/append.js.map +1 -0
  161. package/dist/workflows/events/idempotency.d.ts +77 -0
  162. package/dist/workflows/events/idempotency.d.ts.map +1 -0
  163. package/dist/workflows/events/idempotency.js +116 -0
  164. package/dist/workflows/events/idempotency.js.map +1 -0
  165. package/dist/workflows/events/index.d.ts +7 -0
  166. package/dist/workflows/events/index.d.ts.map +1 -0
  167. package/dist/workflows/events/index.js +7 -0
  168. package/dist/workflows/events/index.js.map +1 -0
  169. package/dist/workflows/events/payloads.d.ts +917 -0
  170. package/dist/workflows/events/payloads.d.ts.map +1 -0
  171. package/dist/workflows/events/payloads.js +337 -0
  172. package/dist/workflows/events/payloads.js.map +1 -0
  173. package/dist/workflows/events/replay.d.ts +238 -0
  174. package/dist/workflows/events/replay.d.ts.map +1 -0
  175. package/dist/workflows/events/replay.js +608 -0
  176. package/dist/workflows/events/replay.js.map +1 -0
  177. package/dist/workflows/events/schema.d.ts +5242 -0
  178. package/dist/workflows/events/schema.d.ts.map +1 -0
  179. package/dist/workflows/events/schema.js +295 -0
  180. package/dist/workflows/events/schema.js.map +1 -0
  181. package/dist/workflows/events/types.d.ts +34 -0
  182. package/dist/workflows/events/types.d.ts.map +1 -0
  183. package/dist/workflows/events/types.js +2 -0
  184. package/dist/workflows/events/types.js.map +1 -0
  185. package/dist/workflows/fanout.d.ts +36 -0
  186. package/dist/workflows/fanout.d.ts.map +1 -0
  187. package/dist/workflows/fanout.js +114 -0
  188. package/dist/workflows/fanout.js.map +1 -0
  189. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +41 -0
  190. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -0
  191. package/dist/workflows/hostExecutors/botmux-schedule.js +121 -0
  192. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -0
  193. package/dist/workflows/hostExecutors/feishu-im.d.ts +12 -0
  194. package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -0
  195. package/dist/workflows/hostExecutors/feishu-im.js +49 -0
  196. package/dist/workflows/hostExecutors/feishu-im.js.map +1 -0
  197. package/dist/workflows/hostExecutors/feishu-reply.d.ts +24 -0
  198. package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -0
  199. package/dist/workflows/hostExecutors/feishu-reply.js +88 -0
  200. package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -0
  201. package/dist/workflows/hostExecutors/feishu-send.d.ts +23 -0
  202. package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -0
  203. package/dist/workflows/hostExecutors/feishu-send.js +124 -0
  204. package/dist/workflows/hostExecutors/feishu-send.js.map +1 -0
  205. package/dist/workflows/hostExecutors/index.d.ts +8 -0
  206. package/dist/workflows/hostExecutors/index.d.ts.map +1 -0
  207. package/dist/workflows/hostExecutors/index.js +8 -0
  208. package/dist/workflows/hostExecutors/index.js.map +1 -0
  209. package/dist/workflows/hostExecutors/protocol.d.ts +42 -0
  210. package/dist/workflows/hostExecutors/protocol.d.ts.map +1 -0
  211. package/dist/workflows/hostExecutors/protocol.js +181 -0
  212. package/dist/workflows/hostExecutors/protocol.js.map +1 -0
  213. package/dist/workflows/hostExecutors/registry.d.ts +10 -0
  214. package/dist/workflows/hostExecutors/registry.d.ts.map +1 -0
  215. package/dist/workflows/hostExecutors/registry.js +36 -0
  216. package/dist/workflows/hostExecutors/registry.js.map +1 -0
  217. package/dist/workflows/hostExecutors/types.d.ts +78 -0
  218. package/dist/workflows/hostExecutors/types.d.ts.map +1 -0
  219. package/dist/workflows/hostExecutors/types.js +2 -0
  220. package/dist/workflows/hostExecutors/types.js.map +1 -0
  221. package/dist/workflows/loader.d.ts +16 -0
  222. package/dist/workflows/loader.d.ts.map +1 -0
  223. package/dist/workflows/loader.js +56 -0
  224. package/dist/workflows/loader.js.map +1 -0
  225. package/dist/workflows/loop.d.ts +50 -0
  226. package/dist/workflows/loop.d.ts.map +1 -0
  227. package/dist/workflows/loop.js +350 -0
  228. package/dist/workflows/loop.js.map +1 -0
  229. package/dist/workflows/ops-projection.d.ts +168 -0
  230. package/dist/workflows/ops-projection.d.ts.map +1 -0
  231. package/dist/workflows/ops-projection.js +707 -0
  232. package/dist/workflows/ops-projection.js.map +1 -0
  233. package/dist/workflows/orchestrator.d.ts +107 -0
  234. package/dist/workflows/orchestrator.d.ts.map +1 -0
  235. package/dist/workflows/orchestrator.js +197 -0
  236. package/dist/workflows/orchestrator.js.map +1 -0
  237. package/dist/workflows/output-binding.d.ts +70 -0
  238. package/dist/workflows/output-binding.d.ts.map +1 -0
  239. package/dist/workflows/output-binding.js +265 -0
  240. package/dist/workflows/output-binding.js.map +1 -0
  241. package/dist/workflows/params.d.ts +61 -0
  242. package/dist/workflows/params.d.ts.map +1 -0
  243. package/dist/workflows/params.js +195 -0
  244. package/dist/workflows/params.js.map +1 -0
  245. package/dist/workflows/resume.d.ts +263 -0
  246. package/dist/workflows/resume.d.ts.map +1 -0
  247. package/dist/workflows/resume.js +808 -0
  248. package/dist/workflows/resume.js.map +1 -0
  249. package/dist/workflows/run-id.d.ts +2 -0
  250. package/dist/workflows/run-id.d.ts.map +1 -0
  251. package/dist/workflows/run-id.js +7 -0
  252. package/dist/workflows/run-id.js.map +1 -0
  253. package/dist/workflows/run-init.d.ts +48 -0
  254. package/dist/workflows/run-init.d.ts.map +1 -0
  255. package/dist/workflows/run-init.js +99 -0
  256. package/dist/workflows/run-init.js.map +1 -0
  257. package/dist/workflows/runs-dir.d.ts +4 -0
  258. package/dist/workflows/runs-dir.d.ts.map +1 -0
  259. package/dist/workflows/runs-dir.js +15 -0
  260. package/dist/workflows/runs-dir.js.map +1 -0
  261. package/dist/workflows/runtime.d.ts +211 -0
  262. package/dist/workflows/runtime.d.ts.map +1 -0
  263. package/dist/workflows/runtime.js +594 -0
  264. package/dist/workflows/runtime.js.map +1 -0
  265. package/dist/workflows/spawn-bot.d.ts +165 -0
  266. package/dist/workflows/spawn-bot.d.ts.map +1 -0
  267. package/dist/workflows/spawn-bot.js +215 -0
  268. package/dist/workflows/spawn-bot.js.map +1 -0
  269. package/dist/workflows/system.d.ts +49 -0
  270. package/dist/workflows/system.d.ts.map +1 -0
  271. package/dist/workflows/system.js +48 -0
  272. package/dist/workflows/system.js.map +1 -0
  273. package/dist/workflows/trigger-run.d.ts +70 -0
  274. package/dist/workflows/trigger-run.d.ts.map +1 -0
  275. package/dist/workflows/trigger-run.js +88 -0
  276. package/dist/workflows/trigger-run.js.map +1 -0
  277. package/dist/workflows/wait.d.ts +120 -0
  278. package/dist/workflows/wait.d.ts.map +1 -0
  279. package/dist/workflows/wait.js +181 -0
  280. package/dist/workflows/wait.js.map +1 -0
  281. package/package.json +3 -3
@@ -19,6 +19,7 @@
19
19
  <nav class="sidebar-nav" aria-label="Dashboard">
20
20
  <a href="#/" data-route="overview" data-i18n="nav.overview">总览</a>
21
21
  <a href="#/sessions" data-route="sessions" data-i18n="nav.sessions">会话</a>
22
+ <a href="#/workflows" data-route="workflows" data-i18n="nav.workflows">工作流(beta)</a>
22
23
  <a href="#/groups" data-route="groups" data-i18n="nav.groups">群组</a>
23
24
  <a href="#/schedules" data-route="schedules" data-i18n="nav.schedules">定时</a>
24
25
  <a href="#/bot-defaults" data-route="bot-defaults" data-i18n="nav.botDefaults">默认 Bot</a>
@@ -19,6 +19,11 @@
19
19
  --warning-soft: #fff7df;
20
20
  --danger: #dc2626;
21
21
  --danger-soft: #feecec;
22
+ --on-accent: #ffffff;
23
+ --on-danger: #ffffff;
24
+ --modal-shadow: 0 24px 80px rgba(0, 0, 0, 0.25);
25
+ --modal-backdrop: rgba(10, 14, 18, 0.48);
26
+ --qr-surface: #ffffff;
22
27
  --shadow: 0 18px 44px rgba(15, 23, 42, 0.08);
23
28
  --radius: 8px;
24
29
  --sidebar-width: 236px;
@@ -46,6 +51,11 @@
46
51
  --warning-soft: rgba(199, 142, 39, 0.16);
47
52
  --danger: #ff7a7a;
48
53
  --danger-soft: rgba(220, 38, 38, 0.16);
54
+ --on-accent: #111418;
55
+ --on-danger: #111418;
56
+ --modal-shadow: 0 24px 80px rgba(0, 0, 0, 0.52);
57
+ --modal-backdrop: rgba(0, 0, 0, 0.62);
58
+ --qr-surface: #ffffff;
49
59
  --shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
50
60
  }
51
61
 
@@ -375,6 +385,20 @@ button,
375
385
  font: inherit;
376
386
  }
377
387
 
388
+ input::placeholder {
389
+ color: var(--faint);
390
+ }
391
+
392
+ button:focus-visible,
393
+ .btn-link:focus-visible,
394
+ input:focus-visible,
395
+ select:focus-visible,
396
+ textarea:focus-visible,
397
+ a:focus-visible {
398
+ outline: 2px solid var(--accent);
399
+ outline-offset: 2px;
400
+ }
401
+
378
402
  .filters input[type=search],
379
403
  .filters input[type=text],
380
404
  .filters select,
@@ -541,14 +565,14 @@ button:disabled {
541
565
 
542
566
  button.contrast {
543
567
  background: var(--danger);
544
- color: #fff;
568
+ color: var(--on-danger);
545
569
  border-color: var(--danger);
546
570
  }
547
571
 
548
572
  .btn-link.primary,
549
573
  button.primary {
550
574
  background: var(--accent);
551
- color: #fff;
575
+ color: var(--on-accent);
552
576
  border-color: var(--accent);
553
577
  font-weight: 700;
554
578
  }
@@ -590,6 +614,7 @@ button.primary {
590
614
  :root[data-theme="dark"] .cli-aiden,
591
615
  :root[data-theme="dark"] .cli-coco {
592
616
  background: var(--surface-muted);
617
+ color: var(--fg);
593
618
  border-color: var(--border);
594
619
  }
595
620
 
@@ -609,11 +634,11 @@ dialog {
609
634
  padding: 0;
610
635
  background: var(--surface);
611
636
  color: var(--fg);
612
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.25);
637
+ box-shadow: var(--modal-shadow);
613
638
  }
614
639
 
615
640
  dialog::backdrop {
616
- background: rgba(10, 14, 18, 0.48);
641
+ background: var(--modal-backdrop);
617
642
  backdrop-filter: blur(3px);
618
643
  }
619
644
 
@@ -773,9 +798,9 @@ input.row-select,
773
798
  .qr-image {
774
799
  width: min(260px, 74vw);
775
800
  height: auto;
776
- border: 10px solid #fff;
801
+ border: 10px solid var(--qr-surface);
777
802
  border-radius: 8px;
778
- background: #fff;
803
+ background: var(--qr-surface);
779
804
  }
780
805
 
781
806
  .onboarding-link {
@@ -931,3 +956,132 @@ input.row-select,
931
956
  overflow-x: auto;
932
957
  }
933
958
  }
959
+
960
+ /* ─── Workflow runtime: list, detail, parallel timeline, IO cards ─── */
961
+ /* Status pills + dangling badges piggyback on the global semantic tokens
962
+ (--accent / --success / --warning / --danger + their *-soft pairs) so
963
+ light <-> dark switches just by flipping data-theme on <html>. */
964
+ .wf-status { padding: 0.05rem 0.4rem; border-radius: 999px; font-size: 11px; font-variant: small-caps; background: var(--bg-soft); color: var(--muted); }
965
+ .wf-status-pending { background: var(--bg-soft); color: var(--muted); }
966
+ .wf-status-running { background: var(--accent-soft); color: var(--accent); }
967
+ .wf-status-waiting { background: var(--warning-soft); color: var(--warning); }
968
+ .wf-status-succeeded { background: var(--success-soft); color: var(--success); }
969
+ .wf-status-failed { background: var(--danger-soft); color: var(--danger); }
970
+ .wf-status-cancelled { background: var(--bg-soft); color: var(--muted); }
971
+ .wf-dangling.has { color: var(--danger); font-weight: 600; }
972
+ .wf-dangling.none { color: var(--muted); }
973
+ .wf-run-error { margin-top: 0.25rem; max-width: 360px; white-space: normal; overflow-wrap: anywhere; color: var(--muted); font-size: 12px; }
974
+
975
+ .wf-subnav { display: flex; gap: 0.4rem; margin-bottom: 0.8rem; border-bottom: 1px solid var(--border); padding-bottom: 0; }
976
+ .wf-subnav a { padding: 0.35rem 0.7rem; color: var(--muted); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
977
+ .wf-subnav a.active { color: var(--fg); border-bottom-color: var(--fg); font-weight: 600; }
978
+ .wf-subnav a:hover { color: var(--fg); }
979
+
980
+ .wf-detail-head { display: flex; align-items: flex-start; gap: 0.8rem; margin-bottom: 0.8rem; }
981
+ .wf-detail-head h2 { margin: 0 0 0.15rem; font-size: 18px; line-height: 1.25; }
982
+ .wf-detail-head #wf-cancel-run { margin-left: auto; }
983
+ .wf-detail-head #wf-detail-refresh { white-space: nowrap; }
984
+ .wf-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.6rem; margin-bottom: 0.8rem; }
985
+ .wf-summary-item { border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.6rem; min-width: 0; background: var(--surface); }
986
+ .wf-summary-item span { display: block; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0; margin-bottom: 0.2rem; }
987
+ .wf-summary-item strong { display: block; font-weight: 600; overflow-wrap: anywhere; }
988
+ .wf-summary-item .wf-status { display: inline-block; }
989
+ .wf-panel { border: 1px solid var(--border); border-radius: 6px; padding: 0.7rem; margin-bottom: 0.8rem; background: var(--surface); }
990
+ .wf-panel-title { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.5rem; }
991
+ .wf-panel-title h3 { margin: 0; font-size: 15px; }
992
+ .wf-panel-title button { margin-left: auto; }
993
+ .wf-dangling-panel.has { border-color: var(--danger); background: var(--danger-soft); }
994
+ .wf-dangling-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.7rem; }
995
+ .wf-dangling-grid ul { margin: 0.25rem 0 0; padding-left: 1.1rem; }
996
+ .wf-dangling-grid li { margin: 0.15rem 0; overflow-wrap: anywhere; }
997
+ .wf-table-scroll { overflow-x: auto; }
998
+ .wf-timeline-scroll { max-height: 520px; overflow: auto; border: 1px solid var(--border); border-radius: 4px; }
999
+ .wf-timeline-scroll table th { position: sticky; top: 0; z-index: 1; }
1000
+ .wf-parallel-axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 12px; margin: 0 0 0.35rem 170px; }
1001
+ .wf-parallel-list { display: grid; gap: 0.45rem; }
1002
+ .wf-parallel-row { display: grid; grid-template-columns: 160px minmax(220px, 1fr); gap: 0.6rem; align-items: center; }
1003
+ .wf-parallel-label { min-width: 0; }
1004
+ .wf-parallel-label code { display: block; overflow-wrap: anywhere; }
1005
+ .wf-parallel-label .muted { display: block; margin-top: 0.1rem; font-size: 11px; overflow-wrap: anywhere; }
1006
+ .wf-parallel-track { position: relative; height: 28px; border: 1px solid var(--border); border-radius: 4px; background: linear-gradient(90deg, var(--bg-soft), var(--surface)); overflow: hidden; }
1007
+ /* Coloured bars sit on top of any theme; foreground stays #fff because the
1008
+ bars carry the strong fill themselves and the contrast holds in both modes. */
1009
+ .wf-parallel-bar { position: absolute; top: 4px; bottom: 4px; min-width: 3px; border-radius: 3px; display: flex; align-items: center; padding: 0 0.35rem; color: #fff; font-size: 11px; line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1010
+ .wf-parallel-running, .wf-parallel-effectAttempting { background: var(--accent); }
1011
+ .wf-parallel-waiting { background: var(--warning); }
1012
+ .wf-parallel-pending { background: #8250df; }
1013
+ .wf-parallel-succeeded { background: var(--success); }
1014
+ .wf-parallel-failed, .wf-parallel-timedOut { background: var(--danger); }
1015
+ .wf-parallel-cancelled { background: var(--faint); }
1016
+ :root[data-theme="dark"] .wf-parallel-running,
1017
+ :root[data-theme="dark"] .wf-parallel-effectAttempting { color: var(--on-accent); }
1018
+ :root[data-theme="dark"] .wf-parallel-waiting,
1019
+ :root[data-theme="dark"] .wf-parallel-succeeded,
1020
+ :root[data-theme="dark"] .wf-parallel-cancelled { color: var(--bg); }
1021
+ :root[data-theme="dark"] .wf-parallel-failed,
1022
+ :root[data-theme="dark"] .wf-parallel-timedOut { color: var(--on-danger); }
1023
+ .wf-io-list { display: grid; gap: 0.7rem; }
1024
+ .wf-io-card { border: 1px solid var(--border); border-radius: 6px; padding: 0.65rem; background: var(--surface); }
1025
+ .wf-io-card.is-focused { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
1026
+ .wf-io-card header { display: flex; justify-content: space-between; gap: 0.8rem; align-items: flex-start; }
1027
+ .wf-io-card header > div:first-child { min-width: 0; }
1028
+ .wf-io-card header code, .wf-io-meta code { overflow-wrap: anywhere; }
1029
+ .wf-io-card header .muted { display: block; margin-top: 0.1rem; overflow-wrap: anywhere; }
1030
+ .wf-io-meta { margin-top: 0.35rem; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
1031
+ .wf-approval-box { margin-top: 0.6rem; padding: 0.55rem; border: 1px solid var(--warning); border-radius: 4px; background: var(--warning-soft); }
1032
+ .wf-approval-box label span { display: block; margin-bottom: 0.25rem; color: var(--muted); font-size: 12px; }
1033
+ .wf-approval-comment { width: 100%; resize: vertical; min-height: 48px; padding: 0.35rem 0.45rem; border: 1px solid var(--border); border-radius: 4px; font: inherit; background: var(--surface); color: var(--fg); }
1034
+ .wf-approval-actions { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.45rem; flex-wrap: wrap; }
1035
+ .wf-approval-status { margin: 0.5rem 0 0; }
1036
+ .wf-io-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.55rem; margin-top: 0.6rem; }
1037
+ .wf-io-block { border: 1px solid var(--border); border-radius: 4px; background: var(--surface-muted); min-width: 0; }
1038
+ .wf-io-block summary { cursor: pointer; padding: 0.4rem 0.5rem; font-weight: 600; }
1039
+ .wf-io-block summary .muted { margin-left: 0.3rem; font-weight: 400; }
1040
+ .wf-io-pre { margin: 0; padding: 0.5rem; max-height: 340px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--surface); color: var(--fg); border-top: 1px solid var(--border); }
1041
+ .wf-io-empty { padding: 0.5rem; }
1042
+ .wf-terminal-block { grid-column: 1 / -1; }
1043
+ .wf-terminal-actions { padding: 0.45rem 0.5rem; border-top: 1px solid var(--border); background: var(--surface); display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
1044
+ .wf-resume-status { padding: 0.35rem 0.5rem; border-top: 1px solid var(--border); }
1045
+ /* Terminal iframe content is itself a CLI surface (Tokyo Night theme), so
1046
+ the wrapper stays dark in both modes to avoid a white flash before xterm
1047
+ boots. */
1048
+ .wf-terminal-frame { display: block; width: 100%; height: 460px; border: 0; border-top: 1px solid var(--border); background: #0d1117; }
1049
+ .wf-error-msg { display: block; margin-top: 0.15rem; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }
1050
+
1051
+ /* Workflow catalog */
1052
+ .catalog-head, .catalog-detail-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.8rem; margin-bottom: 0.8rem; }
1053
+ .catalog-head h2, .catalog-detail-head h2 { margin: 0 0 0.15rem; font-size: 18px; line-height: 1.25; }
1054
+ .catalog-run-form { display: grid; gap: 0.65rem; max-width: 900px; }
1055
+ .catalog-run-form label span, .catalog-param header { display: block; margin-bottom: 0.25rem; color: var(--muted); font-size: 12px; }
1056
+ .catalog-run-form textarea, .catalog-run-form input[type=text] { width: 100%; padding: 0.4rem 0.5rem; border: 1px solid var(--border); border-radius: 4px; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--fg); background: var(--surface); }
1057
+ .catalog-run-form textarea { resize: vertical; min-height: 150px; }
1058
+ .catalog-chat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.65rem; }
1059
+ .catalog-param-errors { background: var(--warning-soft); border-left: 3px solid var(--warning); padding: 0.45rem 0.65rem; border-radius: 3px; }
1060
+ .catalog-param-errors ul { margin: 0.35rem 0 0; padding-left: 1.2rem; }
1061
+ .catalog-param-list { display: grid; gap: 0.55rem; }
1062
+ .catalog-param { border: 1px solid var(--border); border-radius: 4px; padding: 0.55rem; background: var(--surface-muted); }
1063
+ .catalog-param header { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; margin-bottom: 0.35rem; }
1064
+
1065
+ @media (max-width: 720px) {
1066
+ .wf-detail-head { flex-wrap: wrap; align-items: center; }
1067
+ .wf-detail-head > div { min-width: 0; flex: 1 1 220px; }
1068
+ .wf-detail-head h2 code { overflow-wrap: anywhere; }
1069
+ .wf-detail-head #wf-cancel-run { margin-left: 0; }
1070
+ .wf-summary-grid { grid-template-columns: 1fr 1fr; }
1071
+ .wf-panel { padding: 0.55rem; }
1072
+ .wf-parallel-axis { margin-left: 0; }
1073
+ .wf-parallel-row { grid-template-columns: 1fr; gap: 0.25rem; }
1074
+ .wf-io-card header { flex-direction: column; }
1075
+ .wf-io-grid { grid-template-columns: 1fr; }
1076
+ .wf-terminal-frame { height: 360px; }
1077
+ .wf-approval-actions button { flex: 1 1 120px; }
1078
+ .wf-timeline-scroll { max-height: 420px; }
1079
+ .catalog-head, .catalog-detail-head { flex-direction: column; }
1080
+ .catalog-chat-grid { grid-template-columns: 1fr; }
1081
+ }
1082
+
1083
+ @media (max-width: 460px) {
1084
+ .wf-summary-grid { grid-template-columns: 1fr; }
1085
+ .wf-approval-actions { flex-direction: column; align-items: stretch; }
1086
+ .wf-approval-actions button { width: 100%; }
1087
+ }
@@ -0,0 +1,227 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Workflow attempt terminal replay</title>
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css">
7
+ <style>
8
+ html, body {
9
+ margin: 0;
10
+ padding: 0;
11
+ background: #0d1117;
12
+ color: #c9d1d9;
13
+ height: 100%;
14
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
15
+ font-size: 13px;
16
+ }
17
+ #chrome {
18
+ display: flex;
19
+ gap: 1rem;
20
+ align-items: center;
21
+ padding: 0.45rem 0.75rem;
22
+ border-bottom: 1px solid #30363d;
23
+ background: #161b22;
24
+ color: #8b949e;
25
+ font-size: 12px;
26
+ }
27
+ #chrome a {
28
+ color: #58a6ff;
29
+ text-decoration: none;
30
+ }
31
+ #chrome a:hover { text-decoration: underline; }
32
+ #info { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
33
+ #progress { color: #6e7681; margin-left: auto; }
34
+ #term {
35
+ height: calc(100% - 33px);
36
+ padding: 0.25rem;
37
+ box-sizing: border-box;
38
+ }
39
+ #term :focus { outline: none; }
40
+ .mode-toggle {
41
+ display: inline-flex;
42
+ border: 1px solid #30363d;
43
+ border-radius: 4px;
44
+ overflow: hidden;
45
+ }
46
+ .mode-toggle button {
47
+ background: transparent;
48
+ color: #8b949e;
49
+ border: 0;
50
+ padding: 0.2rem 0.6rem;
51
+ font: inherit;
52
+ cursor: pointer;
53
+ }
54
+ .mode-toggle button.active {
55
+ background: #1f6feb;
56
+ color: #ffffff;
57
+ }
58
+ .mode-toggle button:disabled {
59
+ color: #484f58;
60
+ cursor: not-allowed;
61
+ }
62
+ .mode-toggle button:not(.active):not(:disabled):hover {
63
+ background: #21262d;
64
+ color: #c9d1d9;
65
+ }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <div id="chrome">
70
+ <span id="info">Loading…</span>
71
+ <span class="mode-toggle" id="mode-toggle">
72
+ <button type="button" data-mode="pty" id="btn-pty">Terminal</button>
73
+ <button type="button" data-mode="diag" id="btn-diag">Diagnostic</button>
74
+ </span>
75
+ <span id="progress"></span>
76
+ <a id="download-link" href="#" target="_blank" rel="noopener" style="display:none">Download full log</a>
77
+ </div>
78
+ <div id="term"></div>
79
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js"></script>
80
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js"></script>
81
+ <script>
82
+ (function () {
83
+ 'use strict';
84
+ var params = new URLSearchParams(window.location.search);
85
+ var runId = params.get('runId');
86
+ var activityId = params.get('activityId');
87
+ var attemptId = params.get('attemptId');
88
+ // hasPtyLog=1 → projection saw a non-empty pty.log; default mode is the
89
+ // raw PTY cinema. Without it the diag button is the only one enabled
90
+ // (older attempt → no pty.log on disk → /raw?stream=pty would 404).
91
+ var hasPty = params.get('hasPtyLog') === '1';
92
+ var info = document.getElementById('info');
93
+ var progress = document.getElementById('progress');
94
+ var dl = document.getElementById('download-link');
95
+ var btnPty = document.getElementById('btn-pty');
96
+ var btnDiag = document.getElementById('btn-diag');
97
+ if (!runId || !activityId || !attemptId) {
98
+ info.textContent = 'Missing runId/activityId/attemptId query params';
99
+ return;
100
+ }
101
+ var Term = window.Terminal;
102
+ var FitAddon = window.FitAddon ? window.FitAddon.FitAddon : null;
103
+ if (!Term) {
104
+ info.textContent = 'xterm.js failed to load';
105
+ return;
106
+ }
107
+ var term = new Term({
108
+ convertEol: true,
109
+ scrollback: 100000,
110
+ fontSize: 13,
111
+ theme: { background: '#0d1117', foreground: '#c9d1d9' },
112
+ disableStdin: true,
113
+ });
114
+ var fit = FitAddon ? new FitAddon() : null;
115
+ if (fit) term.loadAddon(fit);
116
+ term.open(document.getElementById('term'));
117
+ if (fit) {
118
+ try { fit.fit(); } catch (e) { /* initial fit can race layout */ }
119
+ window.addEventListener('resize', function () {
120
+ try { fit.fit(); } catch (e) { /* ignore */ }
121
+ });
122
+ }
123
+
124
+ var apiBase = '/api/workflows/runs/' + encodeURIComponent(runId)
125
+ + '/attempts/' + encodeURIComponent(activityId)
126
+ + '/' + encodeURIComponent(attemptId)
127
+ + '/terminal-log/raw';
128
+
129
+ var activeFetchToken = 0;
130
+
131
+ function setMode(mode) {
132
+ btnPty.classList.toggle('active', mode === 'pty');
133
+ btnDiag.classList.toggle('active', mode === 'diag');
134
+ var query = mode === 'pty' ? '?stream=pty' : '';
135
+ dl.href = apiBase + (query ? query + '&download=1' : '?download=1');
136
+ dl.style.display = '';
137
+ // Bump token so any in-flight earlier fetch doesn't write into the
138
+ // terminal after we've already reset for the new mode.
139
+ var myToken = ++activeFetchToken;
140
+ term.reset();
141
+ progress.textContent = '';
142
+ info.textContent = 'Loading…';
143
+ fetchAndRender(query, myToken);
144
+ }
145
+
146
+ if (!hasPty) {
147
+ btnPty.disabled = true;
148
+ btnPty.title = 'This attempt has no raw PTY log (older run or empty session)';
149
+ }
150
+
151
+ btnPty.addEventListener('click', function () {
152
+ if (btnPty.disabled || btnPty.classList.contains('active')) return;
153
+ setMode('pty');
154
+ });
155
+ btnDiag.addEventListener('click', function () {
156
+ if (btnDiag.classList.contains('active')) return;
157
+ setMode('diag');
158
+ });
159
+
160
+ // Initial load — PTY when available, diag otherwise.
161
+ setMode(hasPty ? 'pty' : 'diag');
162
+
163
+ function fetchAndRender(query, token) {
164
+ fetch(apiBase + query).then(function (res) {
165
+ if (token !== activeFetchToken) return null;
166
+ if (!res.ok) {
167
+ if (res.status === 404) {
168
+ info.textContent = 'No log on disk for this attempt';
169
+ } else {
170
+ info.textContent = 'Failed to load log: HTTP ' + res.status;
171
+ }
172
+ return null;
173
+ }
174
+ var totalBytes = Number(res.headers.get('x-botmux-log-bytes') || 0);
175
+ var servedBytes = Number(res.headers.get('x-botmux-served-bytes') || 0);
176
+ var truncated = res.headers.get('x-botmux-truncated') === '1';
177
+ info.textContent = truncated
178
+ ? 'Showing last ' + formatBytes(servedBytes) + ' of ' + formatBytes(totalBytes) + ' (truncated — Download for full)'
179
+ : 'Showing ' + formatBytes(totalBytes);
180
+ return res.text();
181
+ }).then(function (text) {
182
+ if (token !== activeFetchToken) return;
183
+ if (text == null) return;
184
+ if (text.length === 0) {
185
+ // header has already filled in totals; nothing to render
186
+ return;
187
+ }
188
+ // Chunk writes through requestAnimationFrame so the browser stays
189
+ // responsive on multi-MB logs. 64 KB / frame ≈ a few seconds for
190
+ // a 10 MB tail at 60 fps.
191
+ var CHUNK = 64 * 1024;
192
+ var offset = 0;
193
+ var total = text.length;
194
+ function writeNext() {
195
+ // Abort the chunk loop if the user switched modes mid-render.
196
+ if (token !== activeFetchToken) return;
197
+ if (offset >= total) {
198
+ progress.textContent = '';
199
+ return;
200
+ }
201
+ var slice = text.slice(offset, offset + CHUNK);
202
+ offset += slice.length;
203
+ term.write(slice);
204
+ progress.textContent = formatBytes(Math.min(offset, total)) + ' / ' + formatBytes(total);
205
+ if (typeof window.requestAnimationFrame === 'function') {
206
+ window.requestAnimationFrame(writeNext);
207
+ } else {
208
+ setTimeout(writeNext, 16);
209
+ }
210
+ }
211
+ writeNext();
212
+ }).catch(function (err) {
213
+ if (token !== activeFetchToken) return;
214
+ info.textContent = 'Failed to load log: ' + (err && err.message ? err.message : err);
215
+ });
216
+ }
217
+
218
+ function formatBytes(n) {
219
+ if (!isFinite(n)) return '0 B';
220
+ if (n < 1024) return n + ' B';
221
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
222
+ return (n / (1024 * 1024)).toFixed(2) + ' MB';
223
+ }
224
+ })();
225
+ </script>
226
+ </body>
227
+ </html>
package/dist/dashboard.js CHANGED
@@ -6,10 +6,12 @@ import { homedir } from 'node:os';
6
6
  import { randomBytes } from 'node:crypto';
7
7
  import { logger } from './utils/logger.js';
8
8
  import { config } from './config.js';
9
- import { generateToken, parseCookie, buildSetCookie, verifyHmac, } from './dashboard/auth.js';
9
+ import { generateToken, parseCookie, buildSetCookie, verifyHmac, decideDashboardAuth, } from './dashboard/auth.js';
10
10
  import { DaemonRegistry } from './dashboard/registry.js';
11
11
  import { Aggregator, subscribeDaemon } from './dashboard/aggregator.js';
12
12
  import { pickCreatorForGroup } from './dashboard/operator-selector.js';
13
+ import { handleWorkflowApi, jsonRes } from './dashboard/workflow-api.js';
14
+ import { getRunsDir } from './workflows/runs-dir.js';
13
15
  import { BotOnboardingManager } from './dashboard/bot-onboarding.js';
14
16
  const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
15
17
  const BOTS_JSON_PATH = join(homedir(), '.botmux', 'bots.json');
@@ -131,10 +133,6 @@ function authedToken(req, url) {
131
133
  return q;
132
134
  return parseCookie(req.headers.cookie);
133
135
  }
134
- function jsonRes(res, status, body) {
135
- res.writeHead(status, { 'content-type': 'application/json' });
136
- res.end(JSON.stringify(body));
137
- }
138
136
  async function proxyToDaemon(larkAppId, daemonPath, init) {
139
137
  const d = registry.getByAppId(larkAppId);
140
138
  if (!d) {
@@ -196,18 +194,23 @@ const server = createServer(async (req, res) => {
196
194
  const fullUrl = `http://${config.dashboard.externalHost}:${config.dashboard.port}/?t=${activeToken}`;
197
195
  return jsonRes(res, 200, { url: fullUrl });
198
196
  }
199
- // All other paths require an authenticated session.
200
- const tok = authedToken(req, url);
201
- if (!tok || tok !== activeToken) {
197
+ const presentedToken = authedToken(req, url);
198
+ const decision = decideDashboardAuth({
199
+ method: req.method ?? 'GET',
200
+ pathname: url.pathname,
201
+ hasTokenParam: url.searchParams.has('t'),
202
+ presentedToken,
203
+ activeToken: activeToken ?? '',
204
+ });
205
+ if (decision.kind === 'deny401') {
202
206
  res.writeHead(401, { 'content-type': 'text/html; charset=utf-8' });
203
207
  res.end('<h1>Token expired</h1><p>Run <code>botmux dashboard</code> to get a fresh URL.</p>');
204
208
  return;
205
209
  }
206
- // First hit with `?t=<token>` sets the cookie + redirects to clean URL.
207
- if (url.searchParams.has('t')) {
210
+ if (decision.kind === 'allow+set-cookie') {
208
211
  res.writeHead(302, {
209
- 'set-cookie': buildSetCookie(tok),
210
- 'location': url.pathname || '/',
212
+ 'set-cookie': buildSetCookie(decision.token),
213
+ 'location': decision.redirectTo,
211
214
  });
212
215
  res.end();
213
216
  return;
@@ -262,6 +265,20 @@ const server = createServer(async (req, res) => {
262
265
  res.end(await upstream.text());
263
266
  return;
264
267
  }
268
+ // ─── Workflows (D0 read-only + D1 cancel mutation) ───────────────────────
269
+ //
270
+ // Dashboard reads runsDir directly (single-process; cross-daemon ownership
271
+ // doesn't matter for read-only). All readers in `ops-projection` are
272
+ // pure: no mkdir, no EventLog instantiation. Unknown / corrupt run → 404.
273
+ // Mutations are intentionally proxied to the owner daemon from
274
+ // chat-binding.larkAppId so only the daemon with live workflow runtime
275
+ // context writes the event log.
276
+ if (await handleWorkflowApi(req, res, url, {
277
+ runsDir: getRunsDir(),
278
+ proxyToDaemon,
279
+ })) {
280
+ return;
281
+ }
265
282
  // ─── Groups (Phase B) ────────────────────────────────────────────────────
266
283
  if (req.method === 'GET' && url.pathname === '/api/groups') {
267
284
  // Fan out: each online daemon returns the chats its bot is in.