botmux 2.107.0 → 2.108.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 (515) hide show
  1. package/README.en.md +17 -14
  2. package/README.md +19 -14
  3. package/dist/adapters/backend/sandbox.d.ts +3 -1
  4. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  5. package/dist/adapters/backend/sandbox.js +19 -3
  6. package/dist/adapters/backend/sandbox.js.map +1 -1
  7. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/tmux-backend.js +16 -1
  9. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  10. package/dist/adapters/backend/tmux-pipe-backend.d.ts +24 -15
  11. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  12. package/dist/adapters/backend/tmux-pipe-backend.js +75 -43
  13. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  14. package/dist/adapters/cli/genius.d.ts.map +1 -1
  15. package/dist/adapters/cli/genius.js +9 -15
  16. package/dist/adapters/cli/genius.js.map +1 -1
  17. package/dist/adapters/cli/grok.d.ts +73 -0
  18. package/dist/adapters/cli/grok.d.ts.map +1 -0
  19. package/dist/adapters/cli/grok.js +298 -0
  20. package/dist/adapters/cli/grok.js.map +1 -0
  21. package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
  22. package/dist/adapters/cli/oh-my-pi.js +133 -12
  23. package/dist/adapters/cli/oh-my-pi.js.map +1 -1
  24. package/dist/adapters/cli/registry.d.ts +2 -1
  25. package/dist/adapters/cli/registry.d.ts.map +1 -1
  26. package/dist/adapters/cli/registry.js +3 -1
  27. package/dist/adapters/cli/registry.js.map +1 -1
  28. package/dist/adapters/cli/shared-hints.d.ts +8 -4
  29. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  30. package/dist/adapters/cli/shared-hints.js +8 -4
  31. package/dist/adapters/cli/shared-hints.js.map +1 -1
  32. package/dist/adapters/cli/types.d.ts +18 -16
  33. package/dist/adapters/cli/types.d.ts.map +1 -1
  34. package/dist/adapters/hook-installer.d.ts +5 -4
  35. package/dist/adapters/hook-installer.d.ts.map +1 -1
  36. package/dist/adapters/hook-installer.js +42 -0
  37. package/dist/adapters/hook-installer.js.map +1 -1
  38. package/dist/cli/send-dispatch.d.ts +21 -0
  39. package/dist/cli/send-dispatch.d.ts.map +1 -1
  40. package/dist/cli/send-dispatch.js +159 -0
  41. package/dist/cli/send-dispatch.js.map +1 -1
  42. package/dist/cli/session-list-liveness.d.ts +22 -0
  43. package/dist/cli/session-list-liveness.d.ts.map +1 -0
  44. package/dist/cli/session-list-liveness.js +22 -0
  45. package/dist/cli/session-list-liveness.js.map +1 -0
  46. package/dist/cli.d.ts.map +1 -1
  47. package/dist/cli.js +182 -33
  48. package/dist/cli.js.map +1 -1
  49. package/dist/core/ask-types.d.ts +4 -0
  50. package/dist/core/ask-types.d.ts.map +1 -1
  51. package/dist/core/ask-types.js +10 -0
  52. package/dist/core/ask-types.js.map +1 -1
  53. package/dist/core/cost-calculator.d.ts.map +1 -1
  54. package/dist/core/cost-calculator.js +35 -13
  55. package/dist/core/cost-calculator.js.map +1 -1
  56. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  57. package/dist/core/dashboard-ipc-server.js +89 -16
  58. package/dist/core/dashboard-ipc-server.js.map +1 -1
  59. package/dist/core/dashboard-rows.d.ts.map +1 -1
  60. package/dist/core/dashboard-rows.js +6 -2
  61. package/dist/core/dashboard-rows.js.map +1 -1
  62. package/dist/core/dashboard-url.d.ts +6 -5
  63. package/dist/core/dashboard-url.d.ts.map +1 -1
  64. package/dist/core/dashboard-url.js +11 -8
  65. package/dist/core/dashboard-url.js.map +1 -1
  66. package/dist/core/maintenance.d.ts +11 -8
  67. package/dist/core/maintenance.d.ts.map +1 -1
  68. package/dist/core/maintenance.js +45 -20
  69. package/dist/core/maintenance.js.map +1 -1
  70. package/dist/core/per-bot-env.d.ts.map +1 -1
  71. package/dist/core/per-bot-env.js +4 -0
  72. package/dist/core/per-bot-env.js.map +1 -1
  73. package/dist/core/session-discovery.d.ts.map +1 -1
  74. package/dist/core/session-discovery.js +4 -0
  75. package/dist/core/session-discovery.js.map +1 -1
  76. package/dist/core/skills/cli-admin-command.d.ts.map +1 -1
  77. package/dist/core/skills/cli-admin-command.js +8 -2
  78. package/dist/core/skills/cli-admin-command.js.map +1 -1
  79. package/dist/core/skills/im-command.d.ts.map +1 -1
  80. package/dist/core/skills/im-command.js +3 -1
  81. package/dist/core/skills/im-command.js.map +1 -1
  82. package/dist/core/skills/sources.d.ts +33 -1
  83. package/dist/core/skills/sources.d.ts.map +1 -1
  84. package/dist/core/skills/sources.js +97 -0
  85. package/dist/core/skills/sources.js.map +1 -1
  86. package/dist/core/skills/types.d.ts +8 -0
  87. package/dist/core/skills/types.d.ts.map +1 -1
  88. package/dist/core/terminal-url.d.ts.map +1 -1
  89. package/dist/core/terminal-url.js +12 -2
  90. package/dist/core/terminal-url.js.map +1 -1
  91. package/dist/core/terminal-write-auth.d.ts +34 -0
  92. package/dist/core/terminal-write-auth.d.ts.map +1 -0
  93. package/dist/core/terminal-write-auth.js +76 -0
  94. package/dist/core/terminal-write-auth.js.map +1 -0
  95. package/dist/core/tmux-recovery.d.ts +11 -0
  96. package/dist/core/tmux-recovery.d.ts.map +1 -0
  97. package/dist/core/tmux-recovery.js +19 -0
  98. package/dist/core/tmux-recovery.js.map +1 -0
  99. package/dist/core/types.d.ts +10 -4
  100. package/dist/core/types.d.ts.map +1 -1
  101. package/dist/core/types.js.map +1 -1
  102. package/dist/core/update-check.d.ts +2 -2
  103. package/dist/core/update-check.js +2 -2
  104. package/dist/core/worker-pool.d.ts +3 -0
  105. package/dist/core/worker-pool.d.ts.map +1 -1
  106. package/dist/core/worker-pool.js +70 -3
  107. package/dist/core/worker-pool.js.map +1 -1
  108. package/dist/daemon.d.ts.map +1 -1
  109. package/dist/daemon.js +15 -8
  110. package/dist/daemon.js.map +1 -1
  111. package/dist/dashboard/auth.d.ts +1 -1
  112. package/dist/dashboard/auth.d.ts.map +1 -1
  113. package/dist/dashboard/auth.js +7 -2
  114. package/dist/dashboard/auth.js.map +1 -1
  115. package/dist/dashboard/bot-payload.d.ts +3 -0
  116. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  117. package/dist/dashboard/bot-payload.js +3 -0
  118. package/dist/dashboard/bot-payload.js.map +1 -1
  119. package/dist/dashboard/connector-api.d.ts.map +1 -1
  120. package/dist/dashboard/connector-api.js +39 -13
  121. package/dist/dashboard/connector-api.js.map +1 -1
  122. package/dist/dashboard/history-senders.d.ts +37 -0
  123. package/dist/dashboard/history-senders.d.ts.map +1 -0
  124. package/dist/dashboard/history-senders.js +60 -0
  125. package/dist/dashboard/history-senders.js.map +1 -0
  126. package/dist/dashboard/roles-batch.d.ts +31 -0
  127. package/dist/dashboard/roles-batch.d.ts.map +1 -0
  128. package/dist/dashboard/roles-batch.js +87 -0
  129. package/dist/dashboard/roles-batch.js.map +1 -0
  130. package/dist/dashboard/settings-card-model.d.ts +4 -2
  131. package/dist/dashboard/settings-card-model.d.ts.map +1 -1
  132. package/dist/dashboard/settings-card-model.js +13 -5
  133. package/dist/dashboard/settings-card-model.js.map +1 -1
  134. package/dist/dashboard/settings-write-applier.d.ts +4 -1
  135. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  136. package/dist/dashboard/settings-write-applier.js +6 -1
  137. package/dist/dashboard/settings-write-applier.js.map +1 -1
  138. package/dist/dashboard/skill-install-request.d.ts +4 -0
  139. package/dist/dashboard/skill-install-request.d.ts.map +1 -1
  140. package/dist/dashboard/skill-install-request.js +11 -1
  141. package/dist/dashboard/skill-install-request.js.map +1 -1
  142. package/dist/dashboard/trigger-api.d.ts +9 -1
  143. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  144. package/dist/dashboard/trigger-api.js +48 -10
  145. package/dist/dashboard/trigger-api.js.map +1 -1
  146. package/dist/dashboard/web/app.d.ts.map +1 -1
  147. package/dist/dashboard/web/app.js +286 -271
  148. package/dist/dashboard/web/app.js.map +1 -1
  149. package/dist/dashboard/web/bot-defaults-page.d.ts +26 -0
  150. package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
  151. package/dist/dashboard/web/bot-defaults-page.js +1408 -12
  152. package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
  153. package/dist/dashboard/web/bot-defaults.d.ts +91 -4
  154. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  155. package/dist/dashboard/web/bot-defaults.js +43 -2141
  156. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  157. package/dist/dashboard/web/bot-onboarding.d.ts +6 -1
  158. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  159. package/dist/dashboard/web/bot-onboarding.js +261 -306
  160. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  161. package/dist/dashboard/web/connectors-page.d.ts +4 -0
  162. package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
  163. package/dist/dashboard/web/connectors-page.js +228 -48
  164. package/dist/dashboard/web/connectors-page.js.map +1 -1
  165. package/dist/dashboard/web/dashboard-api.d.ts +9 -0
  166. package/dist/dashboard/web/dashboard-api.d.ts.map +1 -0
  167. package/dist/dashboard/web/dashboard-api.js +18 -0
  168. package/dist/dashboard/web/dashboard-api.js.map +1 -0
  169. package/dist/dashboard/web/dashboard-components.d.ts +92 -0
  170. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -0
  171. package/dist/dashboard/web/dashboard-components.js +250 -0
  172. package/dist/dashboard/web/dashboard-components.js.map +1 -0
  173. package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
  174. package/dist/dashboard/web/dashboard-routes.js +30 -183
  175. package/dist/dashboard/web/dashboard-routes.js.map +1 -1
  176. package/dist/dashboard/web/floating-scrollbars.d.ts +2 -0
  177. package/dist/dashboard/web/floating-scrollbars.d.ts.map +1 -0
  178. package/dist/dashboard/web/floating-scrollbars.js +264 -0
  179. package/dist/dashboard/web/floating-scrollbars.js.map +1 -0
  180. package/dist/dashboard/web/groups-api.d.ts +36 -0
  181. package/dist/dashboard/web/groups-api.d.ts.map +1 -0
  182. package/dist/dashboard/web/groups-api.js +44 -0
  183. package/dist/dashboard/web/groups-api.js.map +1 -0
  184. package/dist/dashboard/web/groups-page.d.ts.map +1 -1
  185. package/dist/dashboard/web/groups-page.js +746 -15
  186. package/dist/dashboard/web/groups-page.js.map +1 -1
  187. package/dist/dashboard/web/groups.d.ts +51 -18
  188. package/dist/dashboard/web/groups.d.ts.map +1 -1
  189. package/dist/dashboard/web/groups.js +145 -1096
  190. package/dist/dashboard/web/groups.js.map +1 -1
  191. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  192. package/dist/dashboard/web/i18n.js +545 -255
  193. package/dist/dashboard/web/i18n.js.map +1 -1
  194. package/dist/dashboard/web/insights-page.d.ts.map +1 -1
  195. package/dist/dashboard/web/insights-page.js +1047 -14
  196. package/dist/dashboard/web/insights-page.js.map +1 -1
  197. package/dist/dashboard/web/insights.d.ts +177 -7
  198. package/dist/dashboard/web/insights.d.ts.map +1 -1
  199. package/dist/dashboard/web/insights.js +187 -1926
  200. package/dist/dashboard/web/insights.js.map +1 -1
  201. package/dist/dashboard/web/legacy-workflow-page.d.ts +3 -0
  202. package/dist/dashboard/web/legacy-workflow-page.d.ts.map +1 -0
  203. package/dist/dashboard/web/legacy-workflow-page.js +717 -0
  204. package/dist/dashboard/web/legacy-workflow-page.js.map +1 -0
  205. package/dist/dashboard/web/monitor-room.d.ts.map +1 -1
  206. package/dist/dashboard/web/monitor-room.js +10 -8
  207. package/dist/dashboard/web/monitor-room.js.map +1 -1
  208. package/dist/dashboard/web/monitoring-page.d.ts.map +1 -1
  209. package/dist/dashboard/web/monitoring-page.js +188 -14
  210. package/dist/dashboard/web/monitoring-page.js.map +1 -1
  211. package/dist/dashboard/web/office-page.d.ts.map +1 -1
  212. package/dist/dashboard/web/office-page.js +11 -23
  213. package/dist/dashboard/web/office-page.js.map +1 -1
  214. package/dist/dashboard/web/overview-page.d.ts.map +1 -1
  215. package/dist/dashboard/web/overview-page.js +141 -147
  216. package/dist/dashboard/web/overview-page.js.map +1 -1
  217. package/dist/dashboard/web/overview.d.ts +0 -1
  218. package/dist/dashboard/web/overview.d.ts.map +1 -1
  219. package/dist/dashboard/web/overview.js +3 -275
  220. package/dist/dashboard/web/overview.js.map +1 -1
  221. package/dist/dashboard/web/preferences.d.ts +1 -6
  222. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  223. package/dist/dashboard/web/preferences.js +0 -21
  224. package/dist/dashboard/web/preferences.js.map +1 -1
  225. package/dist/dashboard/web/role-batch.d.ts +9 -0
  226. package/dist/dashboard/web/role-batch.d.ts.map +1 -0
  227. package/dist/dashboard/web/role-batch.js +42 -0
  228. package/dist/dashboard/web/role-batch.js.map +1 -0
  229. package/dist/dashboard/web/roles-page.d.ts.map +1 -1
  230. package/dist/dashboard/web/roles-page.js +538 -13
  231. package/dist/dashboard/web/roles-page.js.map +1 -1
  232. package/dist/dashboard/web/roles.d.ts +98 -1
  233. package/dist/dashboard/web/roles.d.ts.map +1 -1
  234. package/dist/dashboard/web/roles.js +117 -903
  235. package/dist/dashboard/web/roles.js.map +1 -1
  236. package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
  237. package/dist/dashboard/web/schedules-page.js +82 -24
  238. package/dist/dashboard/web/schedules-page.js.map +1 -1
  239. package/dist/dashboard/web/sessions-kanban.d.ts +67 -0
  240. package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -0
  241. package/dist/dashboard/web/sessions-kanban.js +481 -0
  242. package/dist/dashboard/web/sessions-kanban.js.map +1 -0
  243. package/dist/dashboard/web/sessions-page.d.ts +4 -0
  244. package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
  245. package/dist/dashboard/web/sessions-page.js +1633 -18
  246. package/dist/dashboard/web/sessions-page.js.map +1 -1
  247. package/dist/dashboard/web/sessions.d.ts +45 -4
  248. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  249. package/dist/dashboard/web/sessions.js +53 -2535
  250. package/dist/dashboard/web/sessions.js.map +1 -1
  251. package/dist/dashboard/web/settings-page.d.ts +0 -2
  252. package/dist/dashboard/web/settings-page.d.ts.map +1 -1
  253. package/dist/dashboard/web/settings-page.js +127 -117
  254. package/dist/dashboard/web/settings-page.js.map +1 -1
  255. package/dist/dashboard/web/skills-page.d.ts +10 -1
  256. package/dist/dashboard/web/skills-page.d.ts.map +1 -1
  257. package/dist/dashboard/web/skills-page.js +235 -93
  258. package/dist/dashboard/web/skills-page.js.map +1 -1
  259. package/dist/dashboard/web/team-federation-page.d.ts.map +1 -1
  260. package/dist/dashboard/web/team-federation-page.js +446 -28
  261. package/dist/dashboard/web/team-federation-page.js.map +1 -1
  262. package/dist/dashboard/web/team-federation.d.ts +91 -2
  263. package/dist/dashboard/web/team-federation.d.ts.map +1 -1
  264. package/dist/dashboard/web/team-federation.js +87 -474
  265. package/dist/dashboard/web/team-federation.js.map +1 -1
  266. package/dist/dashboard/web/theme-menu.d.ts +1 -0
  267. package/dist/dashboard/web/theme-menu.d.ts.map +1 -1
  268. package/dist/dashboard/web/theme-menu.js +79 -20
  269. package/dist/dashboard/web/theme-menu.js.map +1 -1
  270. package/dist/dashboard/web/ui.d.ts +1 -2
  271. package/dist/dashboard/web/ui.d.ts.map +1 -1
  272. package/dist/dashboard/web/ui.js +8 -20
  273. package/dist/dashboard/web/ui.js.map +1 -1
  274. package/dist/dashboard/web/v3-api.d.ts +16 -0
  275. package/dist/dashboard/web/v3-api.d.ts.map +1 -0
  276. package/dist/dashboard/web/v3-api.js +21 -0
  277. package/dist/dashboard/web/v3-api.js.map +1 -0
  278. package/dist/dashboard/web/v3-components.d.ts +2 -0
  279. package/dist/dashboard/web/v3-components.d.ts.map +1 -0
  280. package/dist/dashboard/web/v3-components.js +290 -0
  281. package/dist/dashboard/web/v3-components.js.map +1 -0
  282. package/dist/dashboard/web/v3-model.d.ts +80 -0
  283. package/dist/dashboard/web/v3-model.d.ts.map +1 -0
  284. package/dist/dashboard/web/v3-model.js +240 -0
  285. package/dist/dashboard/web/v3-model.js.map +1 -0
  286. package/dist/dashboard/web/v3-page.d.ts.map +1 -1
  287. package/dist/dashboard/web/v3-page.js +2 -22
  288. package/dist/dashboard/web/v3-page.js.map +1 -1
  289. package/dist/dashboard/web/v3-terminal.d.ts +14 -0
  290. package/dist/dashboard/web/v3-terminal.d.ts.map +1 -1
  291. package/dist/dashboard/web/v3-terminal.js +31 -19
  292. package/dist/dashboard/web/v3-terminal.js.map +1 -1
  293. package/dist/dashboard/web/webhook-logs-page.d.ts +16 -0
  294. package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -0
  295. package/dist/dashboard/web/webhook-logs-page.js +195 -0
  296. package/dist/dashboard/web/webhook-logs-page.js.map +1 -0
  297. package/dist/dashboard/web/whiteboards-page.d.ts.map +1 -1
  298. package/dist/dashboard/web/whiteboards-page.js +18 -23
  299. package/dist/dashboard/web/whiteboards-page.js.map +1 -1
  300. package/dist/dashboard/web/workflow-version-switch.d.ts +8 -0
  301. package/dist/dashboard/web/workflow-version-switch.d.ts.map +1 -0
  302. package/dist/dashboard/web/workflow-version-switch.js +9 -0
  303. package/dist/dashboard/web/workflow-version-switch.js.map +1 -0
  304. package/dist/dashboard/web/workflows.d.ts +272 -1
  305. package/dist/dashboard/web/workflows.d.ts.map +1 -1
  306. package/dist/dashboard/web/workflows.js +211 -1397
  307. package/dist/dashboard/web/workflows.js.map +1 -1
  308. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  309. package/dist/dashboard/webhook-routes.js +92 -39
  310. package/dist/dashboard/webhook-routes.js.map +1 -1
  311. package/dist/dashboard-web/app.js +1 -65
  312. package/dist/dashboard-web/brand-logo.png +0 -0
  313. package/dist/dashboard-web/chunks/bot-defaults-page-HSQ6HR3R.js +1 -0
  314. package/dist/dashboard-web/chunks/chunk-6TPODDDC.js +1 -0
  315. package/dist/dashboard-web/chunks/chunk-73EJP2MO.js +1 -0
  316. package/dist/dashboard-web/chunks/chunk-BRTZFELS.js +1 -0
  317. package/dist/dashboard-web/chunks/chunk-EVK77ASG.js +1 -0
  318. package/dist/dashboard-web/chunks/chunk-R3FXBFQH.js +1 -0
  319. package/dist/dashboard-web/chunks/chunk-VRLL7SNU.js +1 -0
  320. package/dist/dashboard-web/chunks/chunk-XCPSHD3G.js +37 -0
  321. package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
  322. package/dist/dashboard-web/chunks/chunk-YVRWYGXR.js +1 -0
  323. package/dist/dashboard-web/chunks/chunk-ZJING6FY.js +66 -0
  324. package/dist/dashboard-web/chunks/connectors-page-ODUA5UCW.js +1 -0
  325. package/dist/dashboard-web/chunks/groups-page-OYX5M6NK.js +8 -0
  326. package/dist/dashboard-web/chunks/insights-page-YCQO6OCR.js +22 -0
  327. package/dist/dashboard-web/chunks/legacy-workflow-page-XJAUNONP.js +2 -0
  328. package/dist/dashboard-web/chunks/{monitor-room-RFX4HPS5.js → monitor-room-WTZNYAFV.js} +17 -15
  329. package/dist/dashboard-web/chunks/monitoring-page-R6LJUK7G.js +2 -0
  330. package/dist/dashboard-web/chunks/office-page-4OXK3GNR.js +1 -0
  331. package/dist/dashboard-web/chunks/overview-page-4MVEJY6U.js +1 -0
  332. package/dist/dashboard-web/chunks/roles-page-NQDW27VV.js +1 -0
  333. package/dist/dashboard-web/chunks/schedules-page-JRNKYHOX.js +1 -0
  334. package/dist/dashboard-web/chunks/sessions-page-LF7UVI7B.js +3 -0
  335. package/dist/dashboard-web/chunks/settings-page-WGW7KQQS.js +2 -0
  336. package/dist/dashboard-web/chunks/skills-page-AMD4XP3T.js +3 -0
  337. package/dist/dashboard-web/chunks/team-federation-page-SYG3K4GG.js +1 -0
  338. package/dist/dashboard-web/chunks/v3-page-6NFFWNUK.js +94 -0
  339. package/dist/dashboard-web/chunks/whiteboards-page-FBRUK2AS.js +1 -0
  340. package/dist/dashboard-web/favicon.png +0 -0
  341. package/dist/dashboard-web/index.html +5 -69
  342. package/dist/dashboard-web/style.css +18835 -7211
  343. package/dist/dashboard.js +150 -32
  344. package/dist/dashboard.js.map +1 -1
  345. package/dist/global-config.d.ts +2 -2
  346. package/dist/global-config.d.ts.map +1 -1
  347. package/dist/i18n/en.d.ts.map +1 -1
  348. package/dist/i18n/en.js +3 -0
  349. package/dist/i18n/en.js.map +1 -1
  350. package/dist/i18n/zh.d.ts.map +1 -1
  351. package/dist/i18n/zh.js +3 -0
  352. package/dist/i18n/zh.js.map +1 -1
  353. package/dist/im/lark/card-builder.d.ts +7 -5
  354. package/dist/im/lark/card-builder.d.ts.map +1 -1
  355. package/dist/im/lark/card-builder.js +10 -7
  356. package/dist/im/lark/card-builder.js.map +1 -1
  357. package/dist/im/lark/card-handler.d.ts.map +1 -1
  358. package/dist/im/lark/card-handler.js +25 -1
  359. package/dist/im/lark/card-handler.js.map +1 -1
  360. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  361. package/dist/im/lark/event-dispatcher.js +84 -0
  362. package/dist/im/lark/event-dispatcher.js.map +1 -1
  363. package/dist/platform/binding.d.ts +9 -0
  364. package/dist/platform/binding.d.ts.map +1 -1
  365. package/dist/platform/binding.js +14 -0
  366. package/dist/platform/binding.js.map +1 -1
  367. package/dist/platform/tunnel-client.d.ts.map +1 -1
  368. package/dist/platform/tunnel-client.js +9 -0
  369. package/dist/platform/tunnel-client.js.map +1 -1
  370. package/dist/services/bot-config-store.d.ts +1 -1
  371. package/dist/services/bot-config-store.d.ts.map +1 -1
  372. package/dist/services/bot-config-store.js +11 -5
  373. package/dist/services/bot-config-store.js.map +1 -1
  374. package/dist/services/file-bridge-path.d.ts +8 -0
  375. package/dist/services/file-bridge-path.d.ts.map +1 -0
  376. package/dist/services/file-bridge-path.js +75 -0
  377. package/dist/services/file-bridge-path.js.map +1 -0
  378. package/dist/services/grok-paths.d.ts +27 -0
  379. package/dist/services/grok-paths.d.ts.map +1 -0
  380. package/dist/services/grok-paths.js +106 -0
  381. package/dist/services/grok-paths.js.map +1 -0
  382. package/dist/services/grok-transcript.d.ts +67 -0
  383. package/dist/services/grok-transcript.d.ts.map +1 -0
  384. package/dist/services/grok-transcript.js +630 -0
  385. package/dist/services/grok-transcript.js.map +1 -0
  386. package/dist/services/hermes-session-filter.d.ts +28 -0
  387. package/dist/services/hermes-session-filter.d.ts.map +1 -0
  388. package/dist/services/hermes-session-filter.js +53 -0
  389. package/dist/services/hermes-session-filter.js.map +1 -0
  390. package/dist/services/hermes-transcript.d.ts +3 -0
  391. package/dist/services/hermes-transcript.d.ts.map +1 -1
  392. package/dist/services/hermes-transcript.js +20 -2
  393. package/dist/services/hermes-transcript.js.map +1 -1
  394. package/dist/services/resumable-session-discovery.d.ts +3 -0
  395. package/dist/services/resumable-session-discovery.d.ts.map +1 -1
  396. package/dist/services/resumable-session-discovery.js +6 -1
  397. package/dist/services/resumable-session-discovery.js.map +1 -1
  398. package/dist/services/skill-registry-store.d.ts +9 -0
  399. package/dist/services/skill-registry-store.d.ts.map +1 -1
  400. package/dist/services/skill-registry-store.js +304 -1
  401. package/dist/services/skill-registry-store.js.map +1 -1
  402. package/dist/services/structured-bridge-clis.d.ts +29 -0
  403. package/dist/services/structured-bridge-clis.d.ts.map +1 -0
  404. package/dist/services/structured-bridge-clis.js +76 -0
  405. package/dist/services/structured-bridge-clis.js.map +1 -0
  406. package/dist/services/trigger-log-store.d.ts +59 -0
  407. package/dist/services/trigger-log-store.d.ts.map +1 -1
  408. package/dist/services/trigger-log-store.js +81 -10
  409. package/dist/services/trigger-log-store.js.map +1 -1
  410. package/dist/services/usage-ledger.d.ts +4 -1
  411. package/dist/services/usage-ledger.d.ts.map +1 -1
  412. package/dist/services/usage-ledger.js +48 -10
  413. package/dist/services/usage-ledger.js.map +1 -1
  414. package/dist/services/webhook-audit.d.ts +10 -0
  415. package/dist/services/webhook-audit.d.ts.map +1 -0
  416. package/dist/services/webhook-audit.js +116 -0
  417. package/dist/services/webhook-audit.js.map +1 -0
  418. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  419. package/dist/setup/bot-config-editor.js +2 -0
  420. package/dist/setup/bot-config-editor.js.map +1 -1
  421. package/dist/setup/ensure-tmux.d.ts +26 -12
  422. package/dist/setup/ensure-tmux.d.ts.map +1 -1
  423. package/dist/setup/ensure-tmux.js +85 -18
  424. package/dist/setup/ensure-tmux.js.map +1 -1
  425. package/dist/skills/definitions.d.ts.map +1 -1
  426. package/dist/skills/definitions.js +14 -1
  427. package/dist/skills/definitions.js.map +1 -1
  428. package/dist/skills/injection-mode.d.ts +16 -0
  429. package/dist/skills/injection-mode.d.ts.map +1 -1
  430. package/dist/skills/injection-mode.js +25 -0
  431. package/dist/skills/injection-mode.js.map +1 -1
  432. package/dist/types.d.ts +9 -0
  433. package/dist/types.d.ts.map +1 -1
  434. package/dist/utils/global-install.d.ts +22 -0
  435. package/dist/utils/global-install.d.ts.map +1 -0
  436. package/dist/utils/global-install.js +106 -0
  437. package/dist/utils/global-install.js.map +1 -0
  438. package/dist/utils/install-diagnostics.d.ts +1 -1
  439. package/dist/utils/install-diagnostics.d.ts.map +1 -1
  440. package/dist/utils/install-diagnostics.js +6 -4
  441. package/dist/utils/install-diagnostics.js.map +1 -1
  442. package/dist/utils/install-info.d.ts +2 -0
  443. package/dist/utils/install-info.d.ts.map +1 -1
  444. package/dist/utils/install-info.js +10 -4
  445. package/dist/utils/install-info.js.map +1 -1
  446. package/dist/worker.js +293 -172
  447. package/dist/worker.js.map +1 -1
  448. package/dist/workflows/events/payloads.d.ts +10 -10
  449. package/dist/workflows/events/schema.d.ts +28 -28
  450. package/package.json +8 -7
  451. package/dist/dashboard/web/connectors.d.ts +0 -2
  452. package/dist/dashboard/web/connectors.d.ts.map +0 -1
  453. package/dist/dashboard/web/connectors.js +0 -2
  454. package/dist/dashboard/web/connectors.js.map +0 -1
  455. package/dist/dashboard/web/office.d.ts +0 -2
  456. package/dist/dashboard/web/office.d.ts.map +0 -1
  457. package/dist/dashboard/web/office.js +0 -2
  458. package/dist/dashboard/web/office.js.map +0 -1
  459. package/dist/dashboard/web/schedules.d.ts +0 -2
  460. package/dist/dashboard/web/schedules.d.ts.map +0 -1
  461. package/dist/dashboard/web/schedules.js +0 -2
  462. package/dist/dashboard/web/schedules.js.map +0 -1
  463. package/dist/dashboard/web/settings.d.ts +0 -2
  464. package/dist/dashboard/web/settings.d.ts.map +0 -1
  465. package/dist/dashboard/web/settings.js +0 -2
  466. package/dist/dashboard/web/settings.js.map +0 -1
  467. package/dist/dashboard/web/skills.d.ts +0 -2
  468. package/dist/dashboard/web/skills.d.ts.map +0 -1
  469. package/dist/dashboard/web/skills.js +0 -2
  470. package/dist/dashboard/web/skills.js.map +0 -1
  471. package/dist/dashboard/web/skin-intro.d.ts +0 -4
  472. package/dist/dashboard/web/skin-intro.d.ts.map +0 -1
  473. package/dist/dashboard/web/skin-intro.js +0 -49
  474. package/dist/dashboard/web/skin-intro.js.map +0 -1
  475. package/dist/dashboard/web/v3.d.ts +0 -3
  476. package/dist/dashboard/web/v3.d.ts.map +0 -1
  477. package/dist/dashboard/web/v3.js +0 -539
  478. package/dist/dashboard/web/v3.js.map +0 -1
  479. package/dist/dashboard/web/whiteboards.d.ts +0 -2
  480. package/dist/dashboard/web/whiteboards.d.ts.map +0 -1
  481. package/dist/dashboard/web/whiteboards.js +0 -2
  482. package/dist/dashboard/web/whiteboards.js.map +0 -1
  483. package/dist/dashboard-web/chunks/bot-defaults-page-OCIHPGEU.js +0 -438
  484. package/dist/dashboard-web/chunks/chunk-3N2PPQVD.js +0 -1
  485. package/dist/dashboard-web/chunks/chunk-3UJYRR2Q.js +0 -37
  486. package/dist/dashboard-web/chunks/chunk-5E3MJJL4.js +0 -1
  487. package/dist/dashboard-web/chunks/chunk-7SZRL6FY.js +0 -1
  488. package/dist/dashboard-web/chunks/chunk-JTSE5VP6.js +0 -54
  489. package/dist/dashboard-web/chunks/chunk-LXIK7OUJ.js +0 -1
  490. package/dist/dashboard-web/chunks/chunk-PE6W3C2C.js +0 -1
  491. package/dist/dashboard-web/chunks/chunk-QW54DWU3.js +0 -325
  492. package/dist/dashboard-web/chunks/connectors-IGETYWCP.js +0 -1
  493. package/dist/dashboard-web/chunks/groups-page-MYBS4ZYJ.js +0 -207
  494. package/dist/dashboard-web/chunks/insights-page-UPA32V2S.js +0 -309
  495. package/dist/dashboard-web/chunks/monitoring-page-7T3BESZ2.js +0 -1
  496. package/dist/dashboard-web/chunks/office-page-6DXYOG55.js +0 -1
  497. package/dist/dashboard-web/chunks/overview-page-LJSBCQW4.js +0 -1
  498. package/dist/dashboard-web/chunks/roles-page-ESURTVQH.js +0 -98
  499. package/dist/dashboard-web/chunks/schedules-LML2ISOT.js +0 -1
  500. package/dist/dashboard-web/chunks/sessions-YB73XOTU.js +0 -1
  501. package/dist/dashboard-web/chunks/sessions-page-MCO5QR7Z.js +0 -1
  502. package/dist/dashboard-web/chunks/settings-page-OSDKNVS3.js +0 -2
  503. package/dist/dashboard-web/chunks/skills-page-PXBZJ6P6.js +0 -2
  504. package/dist/dashboard-web/chunks/team-federation-page-TTPVHFPX.js +0 -10
  505. package/dist/dashboard-web/chunks/v3-page-FVPUL665.js +0 -149
  506. package/dist/dashboard-web/chunks/whiteboards-page-HLCTYSSV.js +0 -1
  507. package/dist/dashboard-web/chunks/workflows-FCPM4S4D.js +0 -184
  508. package/dist/dashboard-web/skins/bluearchive-hero.webp +0 -0
  509. package/dist/dashboard-web/skins/dragonball-goku.webp +0 -0
  510. package/dist/dashboard-web/skins/dragonball-wukong.webp +0 -0
  511. package/dist/dashboard-web/skins/genshin-breeze.webp +0 -0
  512. package/dist/dashboard-web/skins/ikun-hero.webp +0 -0
  513. package/dist/dashboard-web/skins/prts-priestess.webp +0 -0
  514. package/dist/dashboard-web/skins/zzz-hero.webp +0 -0
  515. package/dist/dashboard-web/skins/zzz-pattern.svg +0 -25
@@ -1,56 +1,144 @@
1
- // Legacy (v0.2) workflow Run List / Detail pages — kept reachable only at the
2
- // `#/legacy-workflow[/<runId>]` route so the v2 engine's Feishu progress /
3
- // approval cards + daemon "Workflow started: Web:" links still open a working
4
- // detail page. The main "工作流" nav now points at the v3 runs page (#/workflows);
5
- // this page is intentionally out of the top nav.
6
- //
7
- // Polls /api/workflows/runs every 5s while visible. Each row links to
8
- // #/legacy-workflow/<runId> — the Run Detail page (B path) hooks into the
9
- // same hash route.
10
1
  import { t } from './ui.js';
11
- function pageHtml() {
12
- const statusOptions = [
13
- ['', t('workflow.filter.nonTerminal')],
14
- ['all', t('workflow.filter.all')],
15
- ['pending', statusLabel('pending')],
16
- ['running', statusLabel('running')],
17
- ['waiting', statusLabel('waiting')],
18
- ['succeeded', statusLabel('succeeded')],
19
- ['failed', statusLabel('failed')],
20
- ['cancelled', statusLabel('cancelled')],
2
+ export const LEGACY_WORKFLOW_POLL_MS = 5000;
3
+ export const LEGACY_WORKFLOW_DETAIL_POLL_MS = 2000;
4
+ export const LEGACY_WORKFLOW_TERMINAL_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
5
+ const RESUME_REQUIRES_CLI_SESSION_ID = new Set(['antigravity', 'codex-app', 'cursor', 'mira']);
6
+ const RESUME_USES_SESSION_ID = new Set([
7
+ 'aiden',
8
+ 'coco',
9
+ 'claude-code',
10
+ 'seed',
11
+ 'relay',
12
+ 'codex',
13
+ 'mtr',
14
+ 'hermes',
15
+ 'pi',
16
+ 'mir',
17
+ ]);
18
+ export function parseLegacyWorkflowHash(hash) {
19
+ const detailMatch = hash.match(/^#\/legacy-workflow\/([^?#]+)(?:\?([^#]*))?$/);
20
+ if (!detailMatch)
21
+ return { kind: 'list' };
22
+ const params = new URLSearchParams(detailMatch[2] ?? '');
23
+ return {
24
+ kind: 'detail',
25
+ runId: decodeURIComponent(detailMatch[1]),
26
+ focusAttemptId: params.get('attempt') ?? undefined,
27
+ };
28
+ }
29
+ export function legacyWorkflowStatusFilters() {
30
+ return [
31
+ { value: '', label: t('workflow.filter.nonTerminal') },
32
+ { value: 'all', label: t('workflow.filter.all') },
33
+ { value: 'pending', label: statusLabel('pending') },
34
+ { value: 'running', label: statusLabel('running') },
35
+ { value: 'waiting', label: statusLabel('waiting') },
36
+ { value: 'succeeded', label: statusLabel('succeeded') },
37
+ { value: 'failed', label: statusLabel('failed') },
38
+ { value: 'cancelled', label: statusLabel('cancelled') },
21
39
  ];
22
- return `
23
- <nav class="wf-subnav">
24
- <a href="#/legacy-workflow" class="active" data-i18n="workflow.subnav.runs">${escapeHtml(t('workflow.subnav.runs'))}</a>
25
- <a href="#/workflows" data-i18n="workflow.subnav.v3">${escapeHtml(t('workflow.subnav.v3'))}</a>
26
- </nav>
27
- <form id="wf-filters" class="filters">
28
- <input type="search" name="q" placeholder="${escapeHtml(t('workflow.searchPlaceholder'))}" />
29
- <select name="status">
30
- ${statusOptions.map(([value, label]) => `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`).join('')}
31
- </select>
32
- <span id="wf-last-load" class="muted"></span>
33
- </form>
34
- <table>
35
- <thead><tr>
36
- <th>${escapeHtml(t('workflow.table.run'))}</th><th>${escapeHtml(t('workflow.table.workflow'))}</th><th>${escapeHtml(t('workflow.table.status'))}</th>
37
- <th>${escapeHtml(t('workflow.table.lastSeq'))}</th><th>${escapeHtml(t('workflow.table.dangling'))}</th><th>${escapeHtml(t('workflow.table.updated'))}</th>
38
- <th>${escapeHtml(t('workflow.table.chatApp'))}</th>
39
- </tr></thead>
40
- <tbody id="wf-tbody"></tbody>
41
- </table>
42
- `;
43
40
  }
44
- const POLL_MS = 5000;
45
- const DETAIL_POLL_MS = 2000;
46
- const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
47
- function escapeHtml(s) {
48
- return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
41
+ export async function fetchLegacyWorkflowRuns(status, fetcher = fetch) {
42
+ const params = new URLSearchParams();
43
+ if (status === 'all')
44
+ params.set('all', '1');
45
+ else if (status)
46
+ params.set('status', status);
47
+ const response = await fetcher('/api/workflows/runs' + (params.toString() ? `?${params}` : ''));
48
+ if (!response.ok)
49
+ throw new Error(`HTTP ${response.status}`);
50
+ const body = await response.json();
51
+ return body.runs ?? [];
52
+ }
53
+ export async function fetchLegacyWorkflowSnapshot(runId, fetcher = fetch) {
54
+ const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/snapshot`);
55
+ if (response.status === 404)
56
+ throw new Error(t('workflow.detail.unknownRun'));
57
+ if (!response.ok)
58
+ throw new Error(t('workflow.detail.snapshotHttp', { status: response.status }));
59
+ return await response.json();
60
+ }
61
+ export async function fetchLegacyWorkflowEvents(runId, params, fetcher = fetch) {
62
+ const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/events?${params}`);
63
+ if (response.status === 404)
64
+ throw new Error(t('workflow.detail.unknownRun'));
65
+ if (!response.ok)
66
+ throw new Error(t('workflow.detail.eventsHttp', { status: response.status }));
67
+ return await response.json();
68
+ }
69
+ export async function cancelLegacyWorkflowRun(runId, fetcher = fetch) {
70
+ const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
71
+ method: 'POST',
72
+ headers: { 'content-type': 'application/json' },
73
+ body: JSON.stringify({ reason: 'cancelled via dashboard' }),
74
+ });
75
+ if (response.status === 401)
76
+ throw new Error(t('workflow.detail.writeAccessCancel'));
77
+ const body = await response.json().catch(() => ({}));
78
+ if (!response.ok || !body.ok) {
79
+ throw new Error(body.hint ?? body.error ?? t('workflow.detail.cancelHttp', { status: response.status }));
80
+ }
81
+ return body;
82
+ }
83
+ export async function resolveLegacyWorkflowWait(runId, action, comment, fetcher = fetch) {
84
+ const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}/${action}`, {
85
+ method: 'POST',
86
+ headers: { 'content-type': 'application/json' },
87
+ body: JSON.stringify({ comment }),
88
+ });
89
+ if (response.status === 401)
90
+ throw new Error(t('workflow.detail.writeAccessApproval'));
91
+ const body = await response.json().catch(() => ({}));
92
+ if (!response.ok || !body.ok) {
93
+ throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.actionHttp', { action, status: response.status }));
94
+ }
95
+ return body;
96
+ }
97
+ export async function startLegacyWorkflowResumeSession(runId, activityId, attemptId, fetcher = fetch) {
98
+ const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}` +
99
+ `/attempts/${encodeURIComponent(activityId)}` +
100
+ `/${encodeURIComponent(attemptId)}/resume`, {
101
+ method: 'POST',
102
+ headers: { 'content-type': 'application/json' },
103
+ body: JSON.stringify({}),
104
+ });
105
+ if (response.status === 401)
106
+ throw new Error(t('workflow.detail.writeAccessResume'));
107
+ const body = await response.json().catch(() => ({}));
108
+ if (!response.ok || !body.ok || !body.resumeId || !body.url) {
109
+ throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeStartFailed', { status: response.status }));
110
+ }
111
+ return { resumeId: body.resumeId, url: body.url };
112
+ }
113
+ export async function endLegacyWorkflowResumeSession(runId, activityId, attemptId, fetcher = fetch) {
114
+ const response = await fetcher(`/api/workflows/runs/${encodeURIComponent(runId)}` +
115
+ `/attempts/${encodeURIComponent(activityId)}` +
116
+ `/${encodeURIComponent(attemptId)}/resume/end`, {
117
+ method: 'POST',
118
+ headers: { 'content-type': 'application/json' },
119
+ body: JSON.stringify({ reason: 'ended_by_dashboard' }),
120
+ });
121
+ if (response.status === 401)
122
+ throw new Error(t('workflow.detail.writeAccessResume'));
123
+ const body = await response.json().catch(() => ({}));
124
+ if (!response.ok || !body.ok) {
125
+ if (body.error === 'resume_not_running')
126
+ return { ended: false, resumeNotRunning: true };
127
+ throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeEndFailed', { status: response.status }));
128
+ }
129
+ return { ended: true, resumeNotRunning: false };
130
+ }
131
+ export function isTerminalWorkflowStatus(status) {
132
+ return LEGACY_WORKFLOW_TERMINAL_STATUSES.has(status);
49
133
  }
50
- function fmtUpdated(ms) {
134
+ export function statusLabel(status) {
135
+ const key = `workflow.status.${status}`;
136
+ const label = t(key);
137
+ return label === key ? status : label;
138
+ }
139
+ export function fmtUpdated(ms) {
51
140
  const d = new Date(ms);
52
- const now = Date.now();
53
- const diff = now - ms;
141
+ const diff = Date.now() - ms;
54
142
  if (diff < 60_000)
55
143
  return t('time.secondsAgo', { value: Math.max(1, Math.floor(diff / 1000)) });
56
144
  if (diff < 3_600_000)
@@ -59,647 +147,7 @@ function fmtUpdated(ms) {
59
147
  return t('time.hoursAgo', { value: Math.floor(diff / 3_600_000) });
60
148
  return d.toISOString().slice(0, 19).replace('T', ' ');
61
149
  }
62
- function statusBadge(status) {
63
- const cls = TERMINAL.has(status) ? 'wf-status terminal' : 'wf-status live';
64
- return `<span class="${cls} wf-status-${escapeHtml(status)}">${escapeHtml(statusLabel(status))}</span>`;
65
- }
66
- function statusLabel(status) {
67
- const key = `workflow.status.${status}`;
68
- const label = t(key);
69
- return label === key ? status : label;
70
- }
71
- export function renderWorkflowsPage(root) {
72
- const detailMatch = location.hash.match(/^#\/legacy-workflow\/([^?#]+)(?:\?([^#]*))?$/);
73
- if (detailMatch) {
74
- const params = new URLSearchParams(detailMatch[2] ?? '');
75
- return renderWorkflowDetailPage(root, decodeURIComponent(detailMatch[1]), {
76
- focusAttemptId: params.get('attempt') ?? undefined,
77
- });
78
- }
79
- return renderWorkflowListPage(root);
80
- }
81
- function renderWorkflowListPage(root) {
82
- root.innerHTML = pageHtml();
83
- const tbody = root.querySelector('#wf-tbody');
84
- const form = root.querySelector('#wf-filters');
85
- const lastLoadEl = root.querySelector('#wf-last-load');
86
- let cache = [];
87
- let timer = null;
88
- let inflight = false;
89
- let lastErr = null;
90
- let disposed = false;
91
- function applyFilters(rows) {
92
- const f = new FormData(form);
93
- const q = (f.get('q') ?? '').trim().toLowerCase();
94
- if (!q)
95
- return rows;
96
- return rows.filter((r) => r.runId.toLowerCase().includes(q) ||
97
- r.workflowId.toLowerCase().includes(q) ||
98
- (r.chatId ?? '').toLowerCase().includes(q));
99
- }
100
- function rerender() {
101
- const rows = applyFilters(cache);
102
- if (rows.length === 0) {
103
- tbody.innerHTML = `<tr><td colspan="7" class="empty">${lastErr
104
- ? escapeHtml(t('workflow.list.failedLoad', { error: lastErr }))
105
- : cache.length === 0
106
- ? escapeHtml(t('workflow.list.noRuns'))
107
- : escapeHtml(t('workflow.list.noFilterMatch'))}</td></tr>`;
108
- return;
109
- }
110
- tbody.innerHTML = rows
111
- .map((r) => {
112
- const dangling = `${r.dEf}/${r.dAct}/${r.dWait}`;
113
- const danglingCls = r.dEf + r.dAct + r.dWait > 0 ? 'wf-dangling has' : 'wf-dangling none';
114
- const chatBits = [];
115
- if (r.chatId)
116
- chatBits.push(escapeHtml(r.chatId));
117
- if (r.larkAppId)
118
- chatBits.push(`<span class="muted">${escapeHtml(r.larkAppId)}</span>`);
119
- const chatCell = chatBits.length > 0 ? chatBits.join('<br/>') : '—';
120
- const errorSummary = renderRunErrorSummary(r);
121
- return `<tr data-runid="${escapeHtml(r.runId)}">
122
- <td><a href="#/legacy-workflow/${encodeURIComponent(r.runId)}"><code>${escapeHtml(r.runId)}</code></a></td>
123
- <td>${escapeHtml(r.workflowId)}</td>
124
- <td>${statusBadge(r.status)}${r.failedNodeId ? ` <span class="muted">(${escapeHtml(r.failedNodeId)})</span>` : ''}${errorSummary}</td>
125
- <td>${r.lastSeq}</td>
126
- <td class="${danglingCls}">${dangling}</td>
127
- <td title="${escapeHtml(new Date(r.updatedAt).toISOString())}">${fmtUpdated(r.updatedAt)}</td>
128
- <td>${chatCell}</td>
129
- </tr>`;
130
- })
131
- .join('');
132
- }
133
- function setStatusLine() {
134
- if (lastErr) {
135
- lastLoadEl.textContent = t('workflow.list.error', { error: lastErr });
136
- lastLoadEl.classList.add('error');
137
- }
138
- else {
139
- lastLoadEl.textContent = t('workflow.list.loaded', {
140
- count: cache.length,
141
- time: new Date().toLocaleTimeString(),
142
- });
143
- lastLoadEl.classList.remove('error');
144
- }
145
- }
146
- async function poll() {
147
- if (disposed || inflight)
148
- return;
149
- if (document.hidden)
150
- return;
151
- inflight = true;
152
- try {
153
- const status = form.elements.namedItem('status')?.value ?? '';
154
- const params = new URLSearchParams();
155
- if (status === 'all')
156
- params.set('all', '1');
157
- else if (status)
158
- params.set('status', status);
159
- const url = '/api/workflows/runs' + (params.toString() ? `?${params}` : '');
160
- const r = await fetch(url);
161
- if (!r.ok) {
162
- lastErr = `HTTP ${r.status}`;
163
- cache = [];
164
- }
165
- else {
166
- const body = (await r.json());
167
- cache = body.runs ?? [];
168
- lastErr = null;
169
- }
170
- }
171
- catch (e) {
172
- lastErr = e?.message ?? String(e);
173
- cache = [];
174
- }
175
- finally {
176
- inflight = false;
177
- if (!disposed) {
178
- rerender();
179
- setStatusLine();
180
- }
181
- }
182
- }
183
- function scheduleNext() {
184
- if (timer !== null)
185
- window.clearTimeout(timer);
186
- timer = window.setTimeout(async () => {
187
- await poll();
188
- if (!disposed)
189
- scheduleNext();
190
- }, POLL_MS);
191
- }
192
- function onVisibility() {
193
- if (document.hidden)
194
- return;
195
- void poll();
196
- }
197
- form.addEventListener('input', () => {
198
- rerender();
199
- // Re-fetch immediately when status filter changes so the server-side
200
- // filter applies; client-side `q` is row-local and doesn't need network.
201
- });
202
- form.addEventListener('change', (e) => {
203
- if (e.target.getAttribute('name') === 'status') {
204
- void poll();
205
- }
206
- });
207
- document.addEventListener('visibilitychange', onVisibility);
208
- // initial fetch + loop
209
- void poll().then(() => {
210
- if (!disposed)
211
- scheduleNext();
212
- });
213
- // Cleanup hook — caller can dispose when navigating away.
214
- return () => {
215
- disposed = true;
216
- if (timer !== null)
217
- window.clearTimeout(timer);
218
- document.removeEventListener('visibilitychange', onVisibility);
219
- };
220
- }
221
- function renderWorkflowDetailPage(root, runId, opts = {}) {
222
- root.innerHTML = `
223
- <div class="wf-detail-head">
224
- <a class="btn-link" href="#/legacy-workflow">${escapeHtml(t('workflow.detail.back'))}</a>
225
- <div>
226
- <h2><code>${escapeHtml(runId)}</code></h2>
227
- <div id="wf-detail-subtitle" class="muted">${escapeHtml(t('workflow.detail.loading'))}</div>
228
- </div>
229
- <button id="wf-cancel-run" type="button" class="contrast" hidden>${escapeHtml(t('workflow.detail.cancel'))}</button>
230
- <span id="wf-detail-refresh" class="muted"></span>
231
- </div>
232
- <section id="wf-detail-error" class="hint-warn" hidden></section>
233
- <section id="wf-cancel-status" class="hint-ok" hidden></section>
234
- <section id="wf-summary" class="wf-summary-grid"></section>
235
- <section id="wf-dangling-panel"></section>
236
- <section class="wf-panel">
237
- <div class="wf-panel-title">
238
- <h3>${escapeHtml(t('workflow.detail.parallel'))}</h3>
239
- <span id="wf-parallel-meta" class="muted"></span>
240
- </div>
241
- <div id="wf-parallel-view"></div>
242
- </section>
243
- <section class="wf-panel">
244
- <div class="wf-panel-title">
245
- <h3>${escapeHtml(t('workflow.detail.nodes'))}</h3>
246
- </div>
247
- <div class="wf-table-scroll">
248
- <table>
249
- <thead><tr>
250
- <th>${escapeHtml(t('workflow.detail.node'))}</th><th>${escapeHtml(t('workflow.detail.nodeStatus'))}</th><th>${escapeHtml(t('workflow.detail.activity'))}</th><th>${escapeHtml(t('workflow.detail.activityStatus'))}</th>
251
- <th>${escapeHtml(t('workflow.detail.attempts'))}</th><th>${escapeHtml(t('workflow.detail.current'))}</th><th>${escapeHtml(t('workflow.detail.detail'))}</th>
252
- </tr></thead>
253
- <tbody id="wf-node-tbody"></tbody>
254
- </table>
255
- </div>
256
- </section>
257
- <section class="wf-panel">
258
- <div class="wf-panel-title">
259
- <h3>${escapeHtml(t('workflow.detail.nodeIO'))}</h3>
260
- </div>
261
- <div id="wf-io-list" class="wf-io-list"></div>
262
- </section>
263
- <section class="wf-panel">
264
- <div class="wf-panel-title">
265
- <h3>${escapeHtml(t('workflow.detail.timeline'))}</h3>
266
- <button id="wf-load-older" type="button" hidden>${escapeHtml(t('workflow.detail.loadOlder'))}</button>
267
- </div>
268
- <div class="wf-table-scroll wf-timeline-scroll">
269
- <table>
270
- <thead><tr>
271
- <th>${escapeHtml(t('workflow.detail.seq'))}</th><th>${escapeHtml(t('workflow.detail.event'))}</th><th>${escapeHtml(t('workflow.detail.actor'))}</th><th>${escapeHtml(t('workflow.detail.node'))}</th><th>${escapeHtml(t('workflow.detail.activity'))}</th><th>${escapeHtml(t('workflow.detail.error'))}</th><th>${escapeHtml(t('workflow.detail.time'))}</th>
272
- </tr></thead>
273
- <tbody id="wf-event-tbody"></tbody>
274
- </table>
275
- </div>
276
- <div id="wf-event-meta" class="muted"></div>
277
- </section>
278
- `;
279
- const subtitle = root.querySelector('#wf-detail-subtitle');
280
- const refresh = root.querySelector('#wf-detail-refresh');
281
- const errorEl = root.querySelector('#wf-detail-error');
282
- const cancelStatusEl = root.querySelector('#wf-cancel-status');
283
- const summaryEl = root.querySelector('#wf-summary');
284
- const danglingEl = root.querySelector('#wf-dangling-panel');
285
- const parallelEl = root.querySelector('#wf-parallel-view');
286
- const parallelMeta = root.querySelector('#wf-parallel-meta');
287
- const nodeTbody = root.querySelector('#wf-node-tbody');
288
- const ioList = root.querySelector('#wf-io-list');
289
- const timelineScroll = root.querySelector('.wf-timeline-scroll');
290
- const eventTbody = root.querySelector('#wf-event-tbody');
291
- const eventMeta = root.querySelector('#wf-event-meta');
292
- const cancelBtn = root.querySelector('#wf-cancel-run');
293
- const loadOlder = root.querySelector('#wf-load-older');
294
- let snapshot = null;
295
- let events = [];
296
- let eventIds = new Set();
297
- let oldestSeq = null;
298
- let newestSeq = null;
299
- let hasOlder = false;
300
- let totalCount = 0;
301
- let timer = null;
302
- let disposed = false;
303
- let inflight = false;
304
- let canceling = false;
305
- const openIOBlocks = new Set();
306
- const ioScrollTops = new Map();
307
- const approvalComments = new Map();
308
- const approvalStatuses = new Map();
309
- const resolvingWaits = new Set();
310
- const resumeSessions = new Map();
311
- const resumePending = new Set();
312
- const resumeErrors = new Map();
313
- // Per-card surgical update state. innerHTML-wiping the whole IO
314
- // list every 2s polls causes terminal iframes inside to be torn off
315
- // the document — the browser then discards the iframe's browsing
316
- // context, and the next render reloads its src, which the user sees
317
- // as flicker. Instead we keep each <article> stable across polls
318
- // and only innerHTML-wipe the head/grid sub-containers; the
319
- // terminal slot (with its iframe) is never detached. Keyed by
320
- // activityId or nodeId so retries don't recreate the card.
321
- const cardElements = new Map();
322
- let timelineScrollTop = 0;
323
- let focusAttemptId = opts.focusAttemptId;
324
- function setError(message) {
325
- if (!message) {
326
- errorEl.hidden = true;
327
- errorEl.textContent = '';
328
- return;
329
- }
330
- errorEl.hidden = false;
331
- errorEl.textContent = message;
332
- }
333
- function setCancelStatus(message) {
334
- if (!message) {
335
- cancelStatusEl.hidden = true;
336
- cancelStatusEl.textContent = '';
337
- return;
338
- }
339
- cancelStatusEl.hidden = false;
340
- cancelStatusEl.textContent = message;
341
- }
342
- async function fetchSnapshot() {
343
- const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/snapshot`);
344
- if (res.status === 404) {
345
- throw new Error(t('workflow.detail.unknownRun'));
346
- }
347
- if (!res.ok)
348
- throw new Error(t('workflow.detail.snapshotHttp', { status: res.status }));
349
- snapshot = (await res.json());
350
- }
351
- async function fetchEvents(params) {
352
- const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/events?${params}`);
353
- if (res.status === 404)
354
- throw new Error(t('workflow.detail.unknownRun'));
355
- if (!res.ok)
356
- throw new Error(t('workflow.detail.eventsHttp', { status: res.status }));
357
- return (await res.json());
358
- }
359
- function mergeEvents(incoming, direction) {
360
- const fresh = incoming.filter((ev) => {
361
- if (eventIds.has(ev.eventId))
362
- return false;
363
- eventIds.add(ev.eventId);
364
- return true;
365
- });
366
- if (fresh.length === 0)
367
- return;
368
- events = direction === 'prepend' ? [...fresh, ...events] : [...events, ...fresh];
369
- events.sort((a, b) => eventSeqFromId(a.eventId) - eventSeqFromId(b.eventId));
370
- }
371
- async function initialLoad() {
372
- await fetchSnapshot();
373
- const win = await fetchEvents(new URLSearchParams({ tail: '100' }));
374
- events = [];
375
- eventIds = new Set();
376
- mergeEvents(win.events, 'append');
377
- oldestSeq = win.oldestSeq;
378
- newestSeq = win.newestSeq;
379
- hasOlder = win.hasOlder;
380
- totalCount = win.totalCount;
381
- rerender();
382
- }
383
- async function poll() {
384
- if (disposed || inflight || document.hidden)
385
- return;
386
- inflight = true;
387
- try {
388
- await fetchSnapshot();
389
- if (newestSeq !== null) {
390
- const win = await fetchEvents(new URLSearchParams({ afterSeq: String(newestSeq), limit: '200' }));
391
- mergeEvents(win.events, 'append');
392
- if (win.newestSeq !== null)
393
- newestSeq = win.newestSeq;
394
- if (oldestSeq === null && win.oldestSeq !== null)
395
- oldestSeq = win.oldestSeq;
396
- totalCount = win.totalCount;
397
- }
398
- else {
399
- const win = await fetchEvents(new URLSearchParams({ tail: '1' }));
400
- mergeEvents(win.events, 'append');
401
- oldestSeq = win.oldestSeq;
402
- newestSeq = win.newestSeq;
403
- hasOlder = win.hasOlder;
404
- totalCount = win.totalCount;
405
- }
406
- setError(null);
407
- rerender();
408
- }
409
- catch (err) {
410
- setError(err?.message ?? String(err));
411
- }
412
- finally {
413
- inflight = false;
414
- }
415
- }
416
- async function loadOlderEvents() {
417
- if (oldestSeq === null || !hasOlder)
418
- return;
419
- loadOlder.disabled = true;
420
- try {
421
- const win = await fetchEvents(new URLSearchParams({ beforeSeq: String(oldestSeq), limit: '100' }));
422
- mergeEvents(win.events, 'prepend');
423
- if (win.oldestSeq !== null)
424
- oldestSeq = win.oldestSeq;
425
- hasOlder = win.hasOlder;
426
- totalCount = win.totalCount;
427
- setError(null);
428
- rerender();
429
- }
430
- catch (err) {
431
- setError(err?.message ?? String(err));
432
- }
433
- finally {
434
- loadOlder.disabled = false;
435
- }
436
- }
437
- async function cancelRun() {
438
- if (!snapshot || TERMINAL.has(snapshot.run.status) || canceling)
439
- return;
440
- if (!snapshot.chatBinding?.larkAppId) {
441
- setError(t('workflow.detail.cancelUnavailable', { runId }));
442
- return;
443
- }
444
- const dangling = danglingSummary(snapshot);
445
- const message = t('workflow.detail.cancelConfirm', { runId, ...dangling });
446
- if (!window.confirm(message))
447
- return;
448
- canceling = true;
449
- cancelBtn.disabled = true;
450
- try {
451
- const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
452
- method: 'POST',
453
- headers: { 'content-type': 'application/json' },
454
- body: JSON.stringify({ reason: 'cancelled via dashboard' }),
455
- });
456
- if (res.status === 401) {
457
- throw new Error(t('workflow.detail.writeAccessCancel'));
458
- }
459
- const body = (await res.json().catch(() => ({})));
460
- if (!res.ok || !body.ok) {
461
- throw new Error(body.hint ?? body.error ?? t('workflow.detail.cancelHttp', { status: res.status }));
462
- }
463
- setCancelStatus(body.pending ? t('workflow.detail.cancelPending') : null);
464
- setError(null);
465
- await poll();
466
- }
467
- catch (err) {
468
- setError(err?.message ?? String(err));
469
- }
470
- finally {
471
- canceling = false;
472
- cancelBtn.disabled = false;
473
- rerender();
474
- }
475
- }
476
- async function startResumeSession(attemptId, activityId) {
477
- if (resumePending.has(attemptId))
478
- return;
479
- resumePending.add(attemptId);
480
- resumeErrors.delete(attemptId);
481
- rerender();
482
- try {
483
- const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}` +
484
- `/attempts/${encodeURIComponent(activityId)}` +
485
- `/${encodeURIComponent(attemptId)}/resume`, {
486
- method: 'POST',
487
- headers: { 'content-type': 'application/json' },
488
- body: JSON.stringify({}),
489
- });
490
- if (res.status === 401) {
491
- throw new Error(t('workflow.detail.writeAccessResume'));
492
- }
493
- const body = (await res.json().catch(() => ({})));
494
- if (!res.ok || !body.ok || !body.resumeId || !body.url) {
495
- throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeStartFailed', { status: res.status }));
496
- }
497
- resumeSessions.set(attemptId, { resumeId: body.resumeId, url: body.url });
498
- }
499
- catch (err) {
500
- const message = err?.message ?? String(err);
501
- resumeErrors.set(attemptId, message);
502
- }
503
- finally {
504
- resumePending.delete(attemptId);
505
- rerender();
506
- }
507
- }
508
- async function endResumeSession(attemptId, activityId) {
509
- if (resumePending.has(attemptId))
510
- return;
511
- resumePending.add(attemptId);
512
- resumeErrors.delete(attemptId);
513
- rerender();
514
- try {
515
- const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}` +
516
- `/attempts/${encodeURIComponent(activityId)}` +
517
- `/${encodeURIComponent(attemptId)}/resume/end`, {
518
- method: 'POST',
519
- headers: { 'content-type': 'application/json' },
520
- body: JSON.stringify({ reason: 'ended_by_dashboard' }),
521
- });
522
- if (res.status === 401) {
523
- throw new Error(t('workflow.detail.writeAccessResume'));
524
- }
525
- const body = (await res.json().catch(() => ({})));
526
- if (!res.ok || !body.ok) {
527
- // `resume_not_running` after a manual server-side end is benign —
528
- // drop the local session so the iframe reverts to replay.
529
- if (body.error === 'resume_not_running') {
530
- resumeSessions.delete(attemptId);
531
- }
532
- else {
533
- throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.resumeEndFailed', { status: res.status }));
534
- }
535
- }
536
- else {
537
- resumeSessions.delete(attemptId);
538
- }
539
- }
540
- catch (err) {
541
- const message = err?.message ?? String(err);
542
- resumeErrors.set(attemptId, message);
543
- }
544
- finally {
545
- resumePending.delete(attemptId);
546
- rerender();
547
- }
548
- }
549
- async function resolveHumanGate(attemptId, action) {
550
- if (resolvingWaits.has(attemptId))
551
- return;
552
- resolvingWaits.add(attemptId);
553
- approvalStatuses.delete(attemptId);
554
- rerender();
555
- try {
556
- const comment = approvalComments.get(attemptId)?.trim() || undefined;
557
- const res = await fetch(`/api/workflows/runs/${encodeURIComponent(runId)}/${action}`, {
558
- method: 'POST',
559
- headers: { 'content-type': 'application/json' },
560
- body: JSON.stringify({ comment }),
561
- });
562
- if (res.status === 401) {
563
- throw new Error(t('workflow.detail.writeAccessApproval'));
564
- }
565
- const body = (await res.json().catch(() => ({})));
566
- if (!res.ok || !body.ok) {
567
- throw new Error(body.hint ?? body.message ?? body.error ?? t('workflow.detail.actionHttp', { action, status: res.status }));
568
- }
569
- const label = action === 'approve' ? t('workflow.detail.approved') : t('workflow.detail.rejected');
570
- approvalStatuses.set(attemptId, {
571
- kind: 'ok',
572
- text: body.alreadyTerminal
573
- ? t('workflow.detail.alreadyTerminal', { label })
574
- : body.pending
575
- ? t('workflow.detail.workflowContinue', { label })
576
- : t('workflow.detail.workflowRefreshing', { label }),
577
- });
578
- setError(null);
579
- await poll();
580
- }
581
- catch (err) {
582
- const message = err?.message ?? String(err);
583
- approvalStatuses.set(attemptId, { kind: 'error', text: message });
584
- setError(message);
585
- }
586
- finally {
587
- resolvingWaits.delete(attemptId);
588
- rerender();
589
- }
590
- }
591
- function rerender() {
592
- if (!snapshot)
593
- return;
594
- timelineScrollTop = timelineScroll.scrollTop;
595
- const run = snapshot.run;
596
- if (TERMINAL.has(run.status))
597
- setCancelStatus(null);
598
- subtitle.innerHTML = `${escapeHtml(run.workflowId ?? '?')} · ${statusBadge(run.status)} · lastSeq ${snapshot.lastSeq}`;
599
- refresh.textContent = t('workflow.detail.refreshed', { time: new Date().toLocaleTimeString() });
600
- cancelBtn.hidden = TERMINAL.has(run.status);
601
- cancelBtn.disabled = canceling || !snapshot.chatBinding?.larkAppId;
602
- cancelBtn.textContent = snapshot.chatBinding?.larkAppId
603
- ? t('workflow.detail.cancel')
604
- : t('workflow.detail.cliCancelOnly');
605
- cancelBtn.title = snapshot.chatBinding?.larkAppId
606
- ? t('workflow.detail.cancelTitle')
607
- : t('workflow.detail.cliCancelTitle', { runId });
608
- renderSummary(summaryEl, snapshot);
609
- renderDangling(danglingEl, snapshot);
610
- renderParallelTimeline(parallelEl, parallelMeta, snapshot, events);
611
- renderNodeActivityRows(nodeTbody, snapshot);
612
- const focusConsumed = renderNodeIO(ioList, snapshot, openIOBlocks, ioScrollTops, {
613
- comments: approvalComments,
614
- statuses: approvalStatuses,
615
- resolving: resolvingWaits,
616
- onResolve: resolveHumanGate,
617
- }, {
618
- sessions: resumeSessions,
619
- pending: resumePending,
620
- errors: resumeErrors,
621
- onStart: startResumeSession,
622
- onEnd: endResumeSession,
623
- }, focusAttemptId, cardElements);
624
- if (focusConsumed)
625
- focusAttemptId = undefined;
626
- renderEvents(eventTbody, events);
627
- timelineScroll.scrollTop = timelineScrollTop;
628
- loadOlder.hidden = !hasOlder;
629
- eventMeta.textContent = t('workflow.detail.eventsLoaded', { loaded: events.length, total: totalCount });
630
- }
631
- function scheduleNext() {
632
- if (timer !== null)
633
- window.clearTimeout(timer);
634
- if (snapshot && TERMINAL.has(snapshot.run.status)) {
635
- timer = null;
636
- return;
637
- }
638
- timer = window.setTimeout(async () => {
639
- await poll();
640
- if (!disposed)
641
- scheduleNext();
642
- }, DETAIL_POLL_MS);
643
- }
644
- function onVisibility() {
645
- if (document.hidden)
646
- return;
647
- void poll().then(() => {
648
- if (!disposed && timer === null)
649
- scheduleNext();
650
- });
651
- }
652
- loadOlder.addEventListener('click', () => void loadOlderEvents());
653
- cancelBtn.addEventListener('click', () => void cancelRun());
654
- document.addEventListener('visibilitychange', onVisibility);
655
- void initialLoad()
656
- .then(() => {
657
- setError(null);
658
- if (!disposed)
659
- scheduleNext();
660
- })
661
- .catch((err) => {
662
- setError(err?.message ?? String(err));
663
- subtitle.textContent = t('workflow.detail.loadFailed');
664
- });
665
- return () => {
666
- disposed = true;
667
- if (timer !== null)
668
- window.clearTimeout(timer);
669
- document.removeEventListener('visibilitychange', onVisibility);
670
- };
671
- }
672
- function renderSummary(el, snap) {
673
- const r = snap.run;
674
- const items = [
675
- [t('workflow.summary.workflow'), escapeHtml(r.workflowId ?? '?')],
676
- [t('workflow.summary.status'), statusBadge(r.status)],
677
- [t('workflow.summary.lastSeq'), String(snap.lastSeq)],
678
- [t('workflow.summary.updated'), escapeHtml(new Date(snap.updatedAt).toLocaleString())],
679
- [t('workflow.summary.revision'), escapeHtml(short(r.revisionId))],
680
- [t('workflow.summary.initiator'), escapeHtml(r.initiator ?? '-')],
681
- ];
682
- if (r.failedNodeId)
683
- items.push([t('workflow.summary.failedNode'), escapeHtml(r.failedNodeId)]);
684
- if (r.cancelOriginEventId)
685
- items.push([t('workflow.summary.cancelOrigin'), escapeHtml(r.cancelOriginEventId)]);
686
- if (snap.chatBinding) {
687
- items.push([t('workflow.summary.chat'), `<code>${escapeHtml(snap.chatBinding.chatId)}</code>`]);
688
- items.push([t('workflow.summary.app'), `<code>${escapeHtml(snap.chatBinding.larkAppId)}</code>`]);
689
- }
690
- el.innerHTML = items
691
- .map(([label, value]) => `<div class="wf-summary-item"><span>${label}</span><strong>${value}</strong></div>`)
692
- .join('');
693
- }
694
- function renderRunErrorSummary(run) {
695
- if (!run.errorCode)
696
- return '';
697
- const message = run.errorMessage ? ` — ${shortText(run.errorMessage, 96)}` : '';
698
- return `<div class="wf-run-error">
699
- <span class="muted error">${escapeHtml(run.errorCode)}</span>${escapeHtml(message)}
700
- </div>`;
701
- }
702
- function danglingSummary(snap) {
150
+ export function danglingSummary(snap) {
703
151
  const d = snap.dangling;
704
152
  return {
705
153
  total: new Set([
@@ -714,58 +162,7 @@ function danglingSummary(snap) {
714
162
  cancels: d.cancels.length,
715
163
  };
716
164
  }
717
- function renderDangling(el, snap) {
718
- const d = snap.dangling;
719
- const groups = [
720
- [t('workflow.dangling.activities'), d.activities],
721
- [t('workflow.dangling.effects'), d.effectAttempted],
722
- [t('workflow.dangling.waits'), d.waits],
723
- [t('workflow.dangling.cancels'), d.cancels],
724
- ];
725
- const total = new Set(groups.flatMap(([, xs]) => xs)).size;
726
- el.className = total > 0 ? 'wf-panel wf-dangling-panel has' : 'wf-panel wf-dangling-panel';
727
- if (total === 0) {
728
- el.innerHTML = `<div class="wf-panel-title"><h3>${escapeHtml(t('workflow.detail.dangling'))}</h3></div><div class="muted">${escapeHtml(t('workflow.detail.noDangling'))}</div>`;
729
- return;
730
- }
731
- el.innerHTML = `<div class="wf-panel-title"><h3>${escapeHtml(t('workflow.detail.dangling'))}</h3><span class="wf-dangling has">${total}</span></div>
732
- <div class="wf-dangling-grid">
733
- ${groups
734
- .map(([name, xs]) => `<div><strong>${name}</strong>${xs.length === 0
735
- ? `<div class="muted">${escapeHtml(t('workflow.detail.none'))}</div>`
736
- : `<ul>${xs.map((x) => `<li><code>${escapeHtml(x)}</code></li>`).join('')}</ul>`}</div>`)
737
- .join('')}
738
- </div>`;
739
- }
740
- function renderParallelTimeline(el, metaEl, snap, events) {
741
- const items = buildAttemptTimeline(events, snap);
742
- if (items.length === 0) {
743
- metaEl.textContent = '';
744
- el.innerHTML = `<div class="empty">${escapeHtml(t('workflow.detail.noParallelData'))}</div>`;
745
- return;
746
- }
747
- const now = Date.now();
748
- const start = Math.min(...items.map((item) => item.startedAt));
749
- const end = Math.max(...items.map((item) => item.endedAt ?? now), start + 1000);
750
- const duration = Math.max(1, end - start);
751
- const maxParallel = maxConcurrency(items, now);
752
- const running = items.filter((item) => !item.endedAt && (item.status === 'running' || item.status === 'effectAttempting')).length;
753
- metaEl.textContent = t('workflow.detail.parallelMeta', {
754
- count: items.length,
755
- max: maxParallel,
756
- running,
757
- });
758
- const rows = items
759
- .sort((a, b) => a.startedAt - b.startedAt || a.activityId.localeCompare(b.activityId))
760
- .map((item) => renderParallelRow(item, start, duration, now))
761
- .join('');
762
- el.innerHTML = `<div class="wf-parallel-axis">
763
- <span title="${escapeHtml(new Date(start).toISOString())}">${escapeHtml(formatClock(start))}</span>
764
- <span title="${escapeHtml(new Date(end).toISOString())}">${escapeHtml(formatClock(end))}</span>
765
- </div>
766
- <div class="wf-parallel-list">${rows}</div>`;
767
- }
768
- function buildAttemptTimeline(events, snap) {
165
+ export function buildAttemptTimeline(events, snap) {
769
166
  const byAttempt = new Map();
770
167
  const activityOwner = new Map(snap.activities.map((activity) => [activity.activityId, activity.ownerNodeId]));
771
168
  for (const event of [...events].sort((a, b) => eventSeqFromId(a.eventId) - eventSeqFromId(b.eventId))) {
@@ -819,30 +216,7 @@ function buildAttemptTimeline(events, snap) {
819
216
  }
820
217
  return [...byAttempt.values()];
821
218
  }
822
- function renderParallelRow(item, start, duration, now) {
823
- const end = item.endedAt ?? now;
824
- const left = clamp(((item.startedAt - start) / duration) * 100, 0, 100);
825
- const width = clamp(((Math.max(end, item.startedAt + 1) - item.startedAt) / duration) * 100, 0.7, 100 - left);
826
- const label = item.nodeId ?? item.activityId;
827
- const attempt = item.attemptNumber !== undefined ? `#${item.attemptNumber}` : short(item.attemptId);
828
- const title = [
829
- `${label} ${item.status}`,
830
- `${new Date(item.startedAt).toISOString()} → ${item.endedAt ? new Date(item.endedAt).toISOString() : t('workflow.detail.parallelNow')}`,
831
- item.endType ? `end: ${item.endType}` : undefined,
832
- ].filter(Boolean).join('\n');
833
- return `<div class="wf-parallel-row">
834
- <div class="wf-parallel-label">
835
- <code>${escapeHtml(label)}</code>
836
- <span class="muted">${escapeHtml(item.activityId)} · ${escapeHtml(attempt)}</span>
837
- </div>
838
- <div class="wf-parallel-track">
839
- <div class="wf-parallel-bar wf-parallel-${escapeHtml(item.status)}" style="left:${left.toFixed(3)}%;width:${width.toFixed(3)}%;" title="${escapeHtml(title)}">
840
- <span>${escapeHtml(statusLabel(item.status))}</span>
841
- </div>
842
- </div>
843
- </div>`;
844
- }
845
- function maxConcurrency(items, now) {
219
+ export function maxConcurrency(items, now) {
846
220
  const points = [];
847
221
  for (const item of items) {
848
222
  points.push({ time: item.startedAt, delta: 1 });
@@ -857,132 +231,13 @@ function maxConcurrency(items, now) {
857
231
  }
858
232
  return max;
859
233
  }
860
- function isTerminalActivityEvent(type) {
861
- return type === 'activitySucceeded' ||
862
- type === 'activityFailed' ||
863
- type === 'activityTimedOut' ||
864
- type === 'activityCanceled';
865
- }
866
- function terminalStatusForEvent(type) {
867
- if (type === 'activitySucceeded')
868
- return 'succeeded';
869
- if (type === 'activityCanceled')
870
- return 'cancelled';
871
- if (type === 'activityTimedOut')
872
- return 'timedOut';
873
- return 'failed';
874
- }
875
- function renderNodeActivityRows(tbody, snap) {
876
- const byId = new Map(snap.activities.map((a) => [a.activityId, a]));
877
- const used = new Set();
878
- const rows = [];
879
- for (const node of snap.nodes) {
880
- const activity = (node.activityId ? byId.get(node.activityId) : undefined) ??
881
- snap.activities.find((a) => a.ownerNodeId === node.nodeId);
882
- if (activity)
883
- used.add(activity.activityId);
884
- rows.push(renderNodeActivityRow(node, activity));
885
- }
886
- for (const activity of snap.activities) {
887
- if (used.has(activity.activityId))
888
- continue;
889
- rows.push(renderNodeActivityRow(undefined, activity));
890
- }
891
- tbody.innerHTML = rows.length > 0
892
- ? rows.join('')
893
- : `<tr><td colspan="7" class="empty">${escapeHtml(t('workflow.detail.noNodes'))}</td></tr>`;
894
- }
895
- function renderNodeActivityRow(node, activity) {
896
- const latest = activity?.attempts[activity.attempts.length - 1];
897
- return `<tr>
898
- <td>${node ? `<code>${escapeHtml(node.nodeId)}</code>` : '<span class="muted">-</span>'}</td>
899
- <td>${node ? statusBadge(node.status) : '<span class="muted">-</span>'}</td>
900
- <td>${activity ? `<code>${escapeHtml(activity.activityId)}</code>` : '<span class="muted">-</span>'}</td>
901
- <td>${activity ? statusBadge(activity.status) : '<span class="muted">-</span>'}</td>
902
- <td>${activity?.attempts.length ?? 0}</td>
903
- <td>${latest ? `<code>${escapeHtml(latest.attemptId)}</code>` : '<span class="muted">-</span>'}</td>
904
- <td>${latest ? renderAttemptDetail(latest) : `<span class="muted">${escapeHtml(t('workflow.detail.idle'))}</span>`}</td>
905
- </tr>`;
906
- }
907
- function renderNodeIO(el, snap, openBlocks, scrollTops, approval, resume, focusAttemptId, cardElements) {
908
- syncIOBlockState(el, openBlocks, scrollTops);
909
- syncApprovalComments(el, approval.comments);
910
- const focusHasTerminal = !!(focusAttemptId && snap.attemptIO?.[focusAttemptId]?.terminal);
911
- if (focusHasTerminal && focusAttemptId) {
912
- openBlocks.add(ioBlockKey(focusAttemptId, t('workflow.detail.liveTerminal')));
913
- }
914
- // Per-card surgical update — we never `innerHTML=`-wipe the parent
915
- // `el` because doing so detaches every terminal iframe from the
916
- // document, which in all major browsers discards the iframe's
917
- // browsing context (the WebSocket included) and the next paint
918
- // reloads the iframe → terminal flickers. Instead we keep each
919
- // <article> stable across polls and only innerHTML-wipe the
920
- // per-card head/grid sub-containers; the terminal-slot div (whose
921
- // iframe holds the live WebSocket) is never detached.
922
- const descriptors = buildCardDescriptors(snap);
923
- const seenKeys = new Set();
924
- if (cardElements) {
925
- for (const desc of descriptors) {
926
- seenKeys.add(desc.key);
927
- let entry = cardElements.get(desc.key);
928
- if (!entry) {
929
- entry = createCardEntry(desc.key);
930
- cardElements.set(desc.key, entry);
931
- el.appendChild(entry.article);
932
- }
933
- updateCardEntry(entry, desc, openBlocks, approval, resume, focusAttemptId);
934
- }
935
- for (const [key, entry] of Array.from(cardElements)) {
936
- if (seenKeys.has(key))
937
- continue;
938
- entry.article.remove();
939
- cardElements.delete(key);
940
- }
941
- if (descriptors.length === 0) {
942
- // Empty-state placeholder. Use a sentinel child rather than
943
- // wiping innerHTML so any (hypothetically lingering) terminal
944
- // iframe wouldn't be detached. Cards are already cleared via
945
- // the loop above, so it's safe to add the placeholder here.
946
- if (!el.querySelector('.wf-io-empty-placeholder')) {
947
- const empty = document.createElement('div');
948
- empty.className = 'empty wf-io-empty-placeholder';
949
- empty.textContent = t('workflow.detail.noNodeIO');
950
- el.appendChild(empty);
951
- }
952
- }
953
- else {
954
- el.querySelector('.wf-io-empty-placeholder')?.remove();
955
- }
956
- }
957
- else {
958
- // No cache provided (unit tests etc.): fall back to bulk innerHTML.
959
- const cards = [];
960
- for (const desc of descriptors) {
961
- cards.push(renderIOCardHtml(desc, openBlocks, approval, resume, focusAttemptId));
962
- }
963
- el.innerHTML = cards.length > 0
964
- ? cards.join('')
965
- : `<div class="empty">${escapeHtml(t('workflow.detail.noNodeIO'))}</div>`;
966
- }
967
- restoreIOBlockScroll(el, scrollTops);
968
- const focusVisible = scrollFocusedAttemptIntoView(el, focusAttemptId);
969
- attachIOBlockToggleTracking(el, openBlocks);
970
- attachIOBlockScrollTracking(el, scrollTops);
971
- attachApprovalControls(el, approval);
972
- attachResumeControls(el, resume);
973
- // Keep the deeplink focus active until terminal.json is visible. The
974
- // progress card link can appear at activityRunning, a little before the
975
- // worker emits ready and writes the terminal sidecar; a later poll should
976
- // still auto-open the terminal block when it arrives.
977
- return focusVisible && focusHasTerminal;
978
- }
979
- function buildCardDescriptors(snap) {
980
- const byId = new Map(snap.activities.map((a) => [a.activityId, a]));
234
+ export function buildCardDescriptors(snap) {
235
+ const byId = new Map(snap.activities.map((activity) => [activity.activityId, activity]));
981
236
  const used = new Set();
982
237
  const out = [];
983
238
  for (const node of snap.nodes) {
984
239
  const activity = (node.activityId ? byId.get(node.activityId) : undefined) ??
985
- snap.activities.find((a) => a.ownerNodeId === node.nodeId);
240
+ snap.activities.find((candidate) => candidate.ownerNodeId === node.nodeId);
986
241
  if (!activity) {
987
242
  out.push({ key: `node:${node.nodeId}`, node });
988
243
  continue;
@@ -1006,260 +261,49 @@ function buildCardDescriptors(snap) {
1006
261
  }
1007
262
  return out;
1008
263
  }
1009
- function createCardEntry(key) {
1010
- const article = document.createElement('article');
1011
- article.className = 'wf-io-card';
1012
- article.dataset.wfCardKey = key;
1013
- const head = document.createElement('div');
1014
- head.className = 'wf-io-card-head';
1015
- const terminalSlot = document.createElement('div');
1016
- terminalSlot.className = 'wf-io-terminal-slot';
1017
- const grid = document.createElement('div');
1018
- grid.className = 'wf-io-grid';
1019
- article.appendChild(head);
1020
- article.appendChild(terminalSlot);
1021
- article.appendChild(grid);
1022
- return { article, head, terminalSlot, grid, currentTerminalUrl: null };
264
+ export function latestAttempt(activity) {
265
+ return activity?.attempts[activity.attempts.length - 1];
1023
266
  }
1024
- function updateCardEntry(entry, desc, openBlocks, approval, resume, focusAttemptId) {
1025
- const attempt = latestAttempt(desc.activity);
1026
- const title = desc.node?.nodeId ?? desc.activity?.ownerNodeId ?? desc.activity?.activityId ?? 'unknown';
1027
- const focusMatch = !!(attempt && attempt.attemptId === focusAttemptId);
1028
- entry.article.classList.toggle('is-focused', focusMatch);
1029
- if (attempt) {
1030
- entry.article.dataset.wfAttemptCard = attempt.attemptId;
1031
- }
1032
- else {
1033
- delete entry.article.dataset.wfAttemptCard;
1034
- }
1035
- // Head: header (title + status badges) + meta (attemptId) + approval controls.
1036
- // These all re-render every poll; no live state survives in them.
1037
- const controls = renderApprovalControls(attempt, approval);
1038
- entry.head.innerHTML = `
1039
- <header>
1040
- <div>
1041
- <strong><code>${escapeHtml(title)}</code></strong>
1042
- <span class="muted">${desc.activity ? escapeHtml(desc.activity.activityId) : escapeHtml(t('workflow.detail.notDispatched'))}</span>
1043
- </div>
1044
- <div>${desc.node ? statusBadge(desc.node.status) : ''} ${desc.activity ? statusBadge(desc.activity.status) : ''}</div>
1045
- </header>
1046
- <div class="wf-io-meta">
1047
- ${attempt ? `${escapeHtml(t('workflow.detail.attempt'))} <code>${escapeHtml(attempt.attemptId)}</code>` : escapeHtml(t('workflow.detail.noAttempt'))}
1048
- </div>
1049
- ${controls}
1050
- `;
1051
- // Terminal slot: only re-render when the URL changes (worker
1052
- // restart / attempt-id flip / live → replay handoff / resume start
1053
- // or end). The iframe DOM node stays live through every same-URL
1054
- // poll so its WebSocket (live / resume) / streaming xterm parser
1055
- // (replay) isn't torn down.
1056
- const desiredSurface = computeTerminalUrl(attempt, desc.activity, desc.io?.terminal, resume);
1057
- const desiredUrl = desiredSurface?.url ?? null;
1058
- if (desiredUrl !== entry.currentTerminalUrl) {
1059
- if (desiredSurface === null) {
1060
- entry.terminalSlot.innerHTML = '';
1061
- }
1062
- else {
1063
- entry.terminalSlot.innerHTML = renderTerminalBlockHtml(desc.key, attempt, desc.activity, desc.io?.terminal, desiredSurface, openBlocks, resume);
1064
- }
1065
- entry.currentTerminalUrl = desiredUrl;
1066
- }
1067
- else if (desiredSurface !== null && desc.io?.terminal) {
1068
- // Same URL — refresh only the summary meta (status badge text)
1069
- // and resume action button state (pending / error) inline. The
1070
- // iframe itself is left alone to preserve its browsing context.
1071
- const summary = entry.terminalSlot.querySelector('details.wf-terminal-block > summary');
1072
- if (summary) {
1073
- const label = terminalSurfaceLabel(desiredSurface.kind);
1074
- summary.innerHTML = `${escapeHtml(label)} ${terminalMeta(attempt, desc.io.terminal)}`;
1075
- }
1076
- if (attempt) {
1077
- refreshResumeActions(entry.terminalSlot, attempt, desc.activity, desc.io.terminal, desiredSurface, resume);
1078
- }
1079
- }
1080
- // Grid: previews other than terminal. Wipe + rebuild — no live
1081
- // resources here so flicker doesn't matter.
1082
- const keyPrefix = attempt?.attemptId ?? desc.activity?.activityId ?? desc.node?.nodeId ?? 'unknown';
1083
- entry.grid.innerHTML = `
1084
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.authoredInput'), desc.io?.input, openBlocks)}
1085
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.resolvedInput'), desc.io?.resolvedInput, openBlocks)}
1086
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.output'), desc.io?.output, openBlocks)}
1087
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.executionLog'), desc.io?.log, openBlocks)}
1088
- ${desc.io?.waitPrompt ? renderPreviewBlock(keyPrefix, t('workflow.detail.waitPrompt'), desc.io.waitPrompt, openBlocks) : ''}
1089
- `;
267
+ export function isOpenHumanGateAttempt(attempt) {
268
+ return !!attempt &&
269
+ attempt.status === 'waiting' &&
270
+ attempt.wait?.waitKind === 'human-gate' &&
271
+ !attempt.wait.resolution;
1090
272
  }
1091
- function computeTerminalUrl(attempt, activity, terminal, resume) {
1092
- if (!terminal)
1093
- return null;
1094
- if (terminal.error)
273
+ export function computeTerminalSurface(options) {
274
+ const { activity, attempt, resumeSession, runId, terminal } = options;
275
+ if (!terminal || terminal.error)
1095
276
  return null;
1096
277
  if (isLiveTerminal(attempt, terminal)) {
1097
- return { kind: 'live', url: terminalReadOnlyUrl(terminal) };
278
+ return { kind: 'live', url: terminalReadOnlyUrl(terminal, options.host) };
1098
279
  }
1099
- if (!attempt || !activity)
1100
- return null;
1101
- if (!isReplayableTerminal(attempt, terminal))
280
+ if (!attempt || !activity || !isReplayableTerminal(attempt, terminal))
1102
281
  return null;
1103
- const runId = currentRunIdFromHash();
1104
- if (!runId)
1105
- return null;
1106
- // Active resume session for this attempt swaps the iframe to the
1107
- // resume worker's write-token PTY URL — keep the download link
1108
- // pointing at the original terminal.log so users can still grab the
1109
- // pre-resume transcript.
1110
- const active = resume?.sessions.get(attempt.attemptId);
1111
- if (active) {
1112
- return {
1113
- kind: 'resume',
1114
- url: active.url,
1115
- resumeId: active.resumeId,
1116
- downloadUrl: terminalLogDownloadUrl(runId, activity.activityId, attempt.attemptId),
1117
- };
282
+ const downloadUrl = terminalLogDownloadUrl(runId, activity.activityId, attempt.attemptId);
283
+ if (resumeSession) {
284
+ return { kind: 'resume', url: resumeSession.url, resumeId: resumeSession.resumeId, downloadUrl };
1118
285
  }
1119
286
  return {
1120
287
  kind: 'replay',
1121
288
  url: terminalReplayPageUrl(runId, activity.activityId, attempt.attemptId, !!terminal.hasPtyLog),
1122
- downloadUrl: terminalLogDownloadUrl(runId, activity.activityId, attempt.attemptId),
289
+ downloadUrl,
1123
290
  };
1124
291
  }
1125
- function renderTerminalBlockHtml(keyPrefix, attempt, activity, terminal, surface, openBlocks, resume) {
1126
- if (!terminal)
1127
- return '';
1128
- const label = terminalSurfaceLabel(surface.kind);
1129
- const key = ioBlockKey(keyPrefix, label);
1130
- const meta = terminalMeta(attempt, terminal);
1131
- const openInTabLabel = terminalOpenInTabLabel(surface.kind);
1132
- const downloadAction = (surface.kind === 'replay' || surface.kind === 'resume')
1133
- ? `<a class="btn-link" href="${escapeHtml(surface.downloadUrl)}" download>${escapeHtml(t('workflow.detail.downloadFullLog'))}</a>`
1134
- : '';
1135
- const resumeAction = attempt
1136
- ? renderResumeButtonHtml(attempt, activity, terminal, surface, resume)
1137
- : '';
1138
- const resumeStatus = attempt ? renderResumeStatusHtml(attempt.attemptId, resume) : '';
1139
- return `<details class="wf-io-block wf-terminal-block" data-io-key="${escapeHtml(key)}"${openBlocks.has(key) ? ' open' : ''}>
1140
- <summary>${escapeHtml(label)} ${meta}</summary>
1141
- <div class="wf-terminal-actions">
1142
- <a class="btn-link" href="${escapeHtml(surface.url)}" target="_blank" rel="noreferrer">${escapeHtml(openInTabLabel)}</a>
1143
- ${downloadAction}
1144
- ${resumeAction}
1145
- </div>
1146
- ${resumeStatus}
1147
- <iframe class="wf-terminal-frame" src="${escapeHtml(surface.url)}" title="${escapeHtml(label)}" loading="lazy"></iframe>
1148
- </details>`;
1149
- }
1150
- function terminalSurfaceLabel(kind) {
292
+ export function terminalSurfaceLabel(kind) {
1151
293
  if (kind === 'live')
1152
294
  return t('workflow.detail.liveTerminal');
1153
295
  if (kind === 'resume')
1154
296
  return t('workflow.detail.terminalResume');
1155
297
  return t('workflow.detail.terminalReplay');
1156
298
  }
1157
- function terminalOpenInTabLabel(kind) {
299
+ export function terminalOpenInTabLabel(kind) {
1158
300
  if (kind === 'live')
1159
301
  return t('workflow.detail.openTerminalNewTab');
1160
302
  if (kind === 'resume')
1161
303
  return t('workflow.detail.openResumeNewTab');
1162
304
  return t('workflow.detail.openReplayNewTab');
1163
305
  }
1164
- /**
1165
- * CLI capability split — mirrors `src/workflows/attempt-resume.ts`:
1166
- * - REQUIRES native cliSessionId: adapter has no botmux-sessionId fallback,
1167
- * so resume can't proceed unless `cli_session_id` IPC was captured.
1168
- * - USES botmux sessionId: adapter resumes by the original attempt sessionId
1169
- * (now passed through as `originalSessionId` in worker init by codex's
1170
- * server-side fix). cliSessionId is consumed when present but optional.
1171
- * - Anything else (opencode / gemini / ...) is `resume_unsupported_cli`
1172
- * server-side.
1173
- */
1174
- const RESUME_REQUIRES_CLI_SESSION_ID = new Set(['antigravity', 'codex-app', 'cursor', 'mira']);
1175
- const RESUME_USES_SESSION_ID = new Set(['aiden', 'coco', 'claude-code', 'seed', 'relay', 'codex', 'mtr', 'hermes', 'pi', 'mir']);
1176
- function isResumeCapableCli(cliId) {
1177
- return !!cliId && (RESUME_USES_SESSION_ID.has(cliId) || RESUME_REQUIRES_CLI_SESSION_ID.has(cliId));
1178
- }
1179
- function cliRequiresNativeSessionId(cliId) {
1180
- return !!cliId && RESUME_REQUIRES_CLI_SESSION_ID.has(cliId);
1181
- }
1182
- function renderResumeButtonHtml(attempt, activity, terminal, surface, resume) {
1183
- if (!resume)
1184
- return '';
1185
- if (surface.kind === 'live')
1186
- return '';
1187
- if (!activity)
1188
- return '';
1189
- const active = surface.kind === 'resume';
1190
- const pending = resume.pending.has(attempt.attemptId);
1191
- const dataAttrs = `data-wf-resume-attempt="${escapeHtml(attempt.attemptId)}" ` +
1192
- `data-wf-resume-activity="${escapeHtml(activity.activityId)}"`;
1193
- // data-wf-resume-button is the stable marker used by refreshResumeActions
1194
- // to locate any prior resume button in-place — including disabled variants
1195
- // that don't carry attempt/activity ids — so re-renders replace instead of
1196
- // appending and creating duplicates each poll.
1197
- if (active) {
1198
- return `<button type="button" class="btn-link" data-wf-resume-button="1" data-wf-resume-action="end" ${dataAttrs}${pending ? ' disabled' : ''}>${escapeHtml(pending ? t('workflow.detail.resumeEnding') : t('workflow.detail.endResumeSession'))}</button>`;
1199
- }
1200
- if (!isResumeCapableCli(terminal.cliId)) {
1201
- return `<button type="button" class="btn-link" data-wf-resume-button="1" disabled title="${escapeHtml(t('workflow.detail.resumeUnsupportedCli', { cliId: terminal.cliId ?? '?' }))}">${escapeHtml(t('workflow.detail.resumeSession'))}</button>`;
1202
- }
1203
- // Only CLIs that have NO botmux-sessionId fallback (antigravity, cursor)
1204
- // hard-require cliSessionId — aiden / coco / claude-code / codex / mtr / hermes resume
1205
- // through the original attempt sessionId on the server side now.
1206
- if (cliRequiresNativeSessionId(terminal.cliId) && !terminal.cliSessionId) {
1207
- return `<button type="button" class="btn-link" data-wf-resume-button="1" disabled title="${escapeHtml(t('workflow.detail.resumeMissingCliSession'))}">${escapeHtml(t('workflow.detail.resumeSession'))}</button>`;
1208
- }
1209
- return `<button type="button" class="btn-link" data-wf-resume-button="1" data-wf-resume-action="start" ${dataAttrs}${pending ? ' disabled' : ''}>${escapeHtml(pending ? t('workflow.detail.resumeStarting') : t('workflow.detail.resumeSession'))}</button>`;
1210
- }
1211
- function renderResumeStatusHtml(attemptId, resume) {
1212
- if (!resume)
1213
- return '';
1214
- const err = resume.errors.get(attemptId);
1215
- if (err) {
1216
- return `<div class="hint-warn wf-resume-status" data-wf-resume-status="${escapeHtml(attemptId)}">${escapeHtml(err)}</div>`;
1217
- }
1218
- return '';
1219
- }
1220
- /**
1221
- * Legacy bulk renderer kept for the no-cache fallback path (tests).
1222
- * The live dashboard always goes through the surgical update path.
1223
- */
1224
- function renderIOCardHtml(desc, openBlocks, approval, resume, focusAttemptId) {
1225
- const attempt = latestAttempt(desc.activity);
1226
- const title = desc.node?.nodeId ?? desc.activity?.ownerNodeId ?? desc.activity?.activityId ?? 'unknown';
1227
- const keyPrefix = attempt?.attemptId ?? desc.activity?.activityId ?? desc.node?.nodeId ?? 'unknown';
1228
- const controls = renderApprovalControls(attempt, approval);
1229
- const focusClass = attempt?.attemptId === focusAttemptId ? ' is-focused' : '';
1230
- const attemptAttr = attempt ? ` data-wf-attempt-card="${escapeHtml(attempt.attemptId)}"` : '';
1231
- const terminalSurface = computeTerminalUrl(attempt, desc.activity, desc.io?.terminal, resume);
1232
- const terminalHtml = terminalSurface
1233
- ? renderTerminalBlockHtml(keyPrefix, attempt, desc.activity, desc.io?.terminal, terminalSurface, openBlocks, resume)
1234
- : '';
1235
- return `<article class="wf-io-card${focusClass}" data-wf-card-key="${escapeHtml(desc.key)}"${attemptAttr}>
1236
- <div class="wf-io-card-head">
1237
- <header>
1238
- <div>
1239
- <strong><code>${escapeHtml(title)}</code></strong>
1240
- <span class="muted">${desc.activity ? escapeHtml(desc.activity.activityId) : escapeHtml(t('workflow.detail.notDispatched'))}</span>
1241
- </div>
1242
- <div>${desc.node ? statusBadge(desc.node.status) : ''} ${desc.activity ? statusBadge(desc.activity.status) : ''}</div>
1243
- </header>
1244
- <div class="wf-io-meta">
1245
- ${attempt ? `${escapeHtml(t('workflow.detail.attempt'))} <code>${escapeHtml(attempt.attemptId)}</code>` : escapeHtml(t('workflow.detail.noAttempt'))}
1246
- </div>
1247
- ${controls}
1248
- </div>
1249
- <div class="wf-io-terminal-slot">${terminalHtml}</div>
1250
- <div class="wf-io-grid">
1251
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.authoredInput'), desc.io?.input, openBlocks)}
1252
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.resolvedInput'), desc.io?.resolvedInput, openBlocks)}
1253
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.output'), desc.io?.output, openBlocks)}
1254
- ${renderPreviewBlock(keyPrefix, t('workflow.detail.executionLog'), desc.io?.log, openBlocks)}
1255
- ${desc.io?.waitPrompt ? renderPreviewBlock(keyPrefix, t('workflow.detail.waitPrompt'), desc.io.waitPrompt, openBlocks) : ''}
1256
- </div>
1257
- </article>`;
1258
- }
1259
- function latestAttempt(activity) {
1260
- return activity?.attempts[activity.attempts.length - 1];
1261
- }
1262
- function terminalMeta(attempt, terminal) {
306
+ export function terminalMetaParts(attempt, terminal) {
1263
307
  const bits = [];
1264
308
  if (terminal.error)
1265
309
  bits.push(t('workflow.detail.error'));
@@ -1269,241 +313,46 @@ function terminalMeta(attempt, terminal) {
1269
313
  bits.push(attempt.status);
1270
314
  if (terminal.webPort > 0)
1271
315
  bits.push(`:${terminal.webPort}`);
1272
- return `<span class="muted">${escapeHtml(bits.join(' · '))}</span>`;
316
+ return bits;
317
+ }
318
+ export function isResumeCapableCli(cliId) {
319
+ return !!cliId && (RESUME_USES_SESSION_ID.has(cliId) || RESUME_REQUIRES_CLI_SESSION_ID.has(cliId));
1273
320
  }
1274
- function isLiveTerminal(attempt, terminal) {
321
+ export function cliRequiresNativeSessionId(cliId) {
322
+ return !!cliId && RESUME_REQUIRES_CLI_SESSION_ID.has(cliId);
323
+ }
324
+ export function isLiveTerminal(attempt, terminal) {
1275
325
  return terminal.status === 'live' &&
1276
326
  terminal.webPort > 0 &&
1277
327
  (attempt?.status === 'pending' || attempt?.status === 'running' || attempt?.status === 'effectAttempting');
1278
328
  }
1279
- /**
1280
- * Replay-eligibility: attempt has reached a terminal state and a worker
1281
- * actually started (sessionId / startedAt non-empty) — meaning a
1282
- * `terminal.log` file likely exists on disk for the replay HTML to fetch.
1283
- * The replay page itself handles a 404 gracefully, but gating here keeps
1284
- * us from rendering an iframe that's guaranteed to show "no log".
1285
- */
1286
- function isReplayableTerminal(attempt, terminal) {
329
+ export function isReplayableTerminal(attempt, terminal) {
1287
330
  const isAttemptTerminal = attempt.status === 'succeeded' ||
1288
331
  attempt.status === 'failed' ||
1289
332
  attempt.status === 'cancelled' ||
1290
333
  attempt.status === 'timedOut';
1291
334
  if (!isAttemptTerminal)
1292
335
  return false;
1293
- const hadWorker = !!(terminal.sessionId || terminal.startedAt);
1294
- return hadWorker;
336
+ return !!(terminal.sessionId || terminal.startedAt);
1295
337
  }
1296
- function terminalReadOnlyUrl(terminal) {
1297
- const host = window.location.hostname || '127.0.0.1';
1298
- return `http://${host}:${terminal.webPort}`;
338
+ export function terminalReadOnlyUrl(terminal, host) {
339
+ const resolvedHost = (host ?? (typeof window !== 'undefined' ? window.location.hostname : '')) || '127.0.0.1';
340
+ return `http://${resolvedHost}:${terminal.webPort}`;
1299
341
  }
1300
- function terminalReplayPageUrl(runId, activityId, attemptId, hasPtyLog) {
1301
- const qs = new URLSearchParams({ runId, activityId, attemptId });
342
+ export function terminalReplayPageUrl(runId, activityId, attemptId, hasPtyLog) {
343
+ const params = new URLSearchParams({ runId, activityId, attemptId });
1302
344
  if (hasPtyLog)
1303
- qs.set('hasPtyLog', '1');
1304
- return `/assets/terminal-replay.html?${qs.toString()}`;
345
+ params.set('hasPtyLog', '1');
346
+ return `/assets/terminal-replay.html?${params.toString()}`;
1305
347
  }
1306
- function terminalLogDownloadUrl(runId, activityId, attemptId) {
348
+ export function terminalLogDownloadUrl(runId, activityId, attemptId) {
1307
349
  return (`/api/workflows/runs/${encodeURIComponent(runId)}` +
1308
350
  `/attempts/${encodeURIComponent(activityId)}` +
1309
351
  `/${encodeURIComponent(attemptId)}/terminal-log/raw?download=1`);
1310
352
  }
1311
- function currentRunIdFromHash() {
1312
- const m = window.location.hash.match(/^#\/legacy-workflow\/([^/?#]+)/);
1313
- if (!m)
1314
- return null;
1315
- try {
1316
- return decodeURIComponent(m[1]);
1317
- }
1318
- catch {
1319
- return null;
1320
- }
1321
- }
1322
- function renderApprovalControls(attempt, approval) {
1323
- if (!isOpenHumanGateAttempt(attempt))
1324
- return '';
1325
- const attemptId = attempt.attemptId;
1326
- const comment = approval.comments.get(attemptId) ?? '';
1327
- const resolving = approval.resolving.has(attemptId);
1328
- const status = approval.statuses.get(attemptId);
1329
- const statusClass = status?.kind === 'error' ? 'hint-warn' : 'hint-ok';
1330
- return `<div class="wf-approval-box" data-wf-approval="${escapeHtml(attemptId)}">
1331
- <label>
1332
- <span>${escapeHtml(t('workflow.detail.approvalComment'))}</span>
1333
- <textarea class="wf-approval-comment" data-wf-approval-comment="${escapeHtml(attemptId)}" rows="2" placeholder="${escapeHtml(t('workflow.detail.optionalComment'))}"${resolving ? ' disabled' : ''}>${escapeHtml(comment)}</textarea>
1334
- </label>
1335
- <div class="wf-approval-actions">
1336
- <button type="button" class="primary" data-wf-approval-action="approve" data-wf-attempt-id="${escapeHtml(attemptId)}"${resolving ? ' disabled' : ''}>${escapeHtml(t('workflow.detail.approve'))}</button>
1337
- <button type="button" data-wf-approval-action="reject" data-wf-attempt-id="${escapeHtml(attemptId)}"${resolving ? ' disabled' : ''}>${escapeHtml(t('workflow.detail.reject'))}</button>
1338
- ${resolving ? `<span class="muted">${escapeHtml(t('workflow.detail.submitting'))}</span>` : ''}
1339
- </div>
1340
- ${status ? `<div class="${statusClass} wf-approval-status">${escapeHtml(status.text)}</div>` : ''}
1341
- </div>`;
1342
- }
1343
- function isOpenHumanGateAttempt(attempt) {
1344
- return !!attempt &&
1345
- attempt.status === 'waiting' &&
1346
- attempt.wait?.waitKind === 'human-gate' &&
1347
- !attempt.wait.resolution;
1348
- }
1349
- function syncApprovalComments(root, comments) {
1350
- root.querySelectorAll('textarea[data-wf-approval-comment]').forEach((el) => {
1351
- const key = el.dataset.wfApprovalComment;
1352
- if (!key)
1353
- return;
1354
- comments.set(key, el.value);
1355
- });
1356
- }
1357
- function attachResumeControls(root, resume) {
1358
- root.querySelectorAll('button[data-wf-resume-action][data-wf-resume-attempt][data-wf-resume-activity]').forEach((button) => {
1359
- if (button.dataset.wfResumeBound === '1')
1360
- return;
1361
- button.dataset.wfResumeBound = '1';
1362
- button.addEventListener('click', () => {
1363
- const attemptId = button.dataset.wfResumeAttempt;
1364
- const activityId = button.dataset.wfResumeActivity;
1365
- const action = button.dataset.wfResumeAction;
1366
- if (!attemptId || !activityId)
1367
- return;
1368
- if (action === 'start')
1369
- void resume.onStart(attemptId, activityId);
1370
- else if (action === 'end')
1371
- void resume.onEnd(attemptId, activityId);
1372
- });
1373
- });
1374
- }
1375
- function refreshResumeActions(slot, attempt, activity, terminal, surface, resume) {
1376
- const actions = slot.querySelector('.wf-terminal-actions');
1377
- if (!actions)
1378
- return;
1379
- // Re-render the resume button area in-place so the surrounding
1380
- // anchors (open-in-tab + download) stay stable. We tag-select the
1381
- // existing button (if any) and replace its outerHTML so the new
1382
- // listener pickup goes through the same data-wf-resume-bound gate.
1383
- const existingButton = actions.querySelector('button[data-wf-resume-button="1"]');
1384
- const html = renderResumeButtonHtml(attempt, activity, terminal, surface, resume);
1385
- if (existingButton) {
1386
- existingButton.outerHTML = html;
1387
- }
1388
- else if (html) {
1389
- actions.insertAdjacentHTML('beforeend', html);
1390
- }
1391
- // Update / clear inline error hint.
1392
- const details = slot.querySelector('details.wf-terminal-block');
1393
- if (details) {
1394
- const existingStatus = details.querySelector('.wf-resume-status');
1395
- const statusHtml = renderResumeStatusHtml(attempt.attemptId, resume);
1396
- if (existingStatus) {
1397
- existingStatus.outerHTML = statusHtml;
1398
- }
1399
- else if (statusHtml) {
1400
- actions.insertAdjacentHTML('afterend', statusHtml);
1401
- }
1402
- }
1403
- attachResumeControls(slot, resume);
1404
- }
1405
- function attachApprovalControls(root, approval) {
1406
- root.querySelectorAll('textarea[data-wf-approval-comment]').forEach((el) => {
1407
- const key = el.dataset.wfApprovalComment;
1408
- if (!key)
1409
- return;
1410
- el.addEventListener('input', () => {
1411
- approval.comments.set(key, el.value);
1412
- });
1413
- });
1414
- root.querySelectorAll('button[data-wf-approval-action][data-wf-attempt-id]').forEach((button) => {
1415
- button.addEventListener('click', () => {
1416
- const attemptId = button.dataset.wfAttemptId;
1417
- const action = button.dataset.wfApprovalAction;
1418
- if (!attemptId || (action !== 'approve' && action !== 'reject'))
1419
- return;
1420
- void approval.onResolve(attemptId, action);
1421
- });
1422
- });
1423
- }
1424
- function renderPreviewBlock(keyPrefix, label, preview, openBlocks) {
1425
- const key = ioBlockKey(keyPrefix, label);
1426
- return `<details class="wf-io-block" data-io-key="${escapeHtml(key)}"${openBlocks.has(key) ? ' open' : ''}>
1427
- <summary>${escapeHtml(label)} ${previewMeta(preview)}</summary>
1428
- ${renderPreview(preview)}
1429
- </details>`;
1430
- }
1431
- function ioBlockKey(keyPrefix, label) {
1432
- return `${keyPrefix}:${label}`;
1433
- }
1434
- function scrollFocusedAttemptIntoView(root, focusAttemptId) {
1435
- if (!focusAttemptId)
1436
- return false;
1437
- for (const card of root.querySelectorAll('[data-wf-attempt-card]')) {
1438
- if (card.dataset.wfAttemptCard !== focusAttemptId)
1439
- continue;
1440
- card.scrollIntoView({ block: 'center' });
1441
- return true;
1442
- }
1443
- return false;
1444
- }
1445
- function syncIOBlockState(root, openBlocks, scrollTops) {
1446
- root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
1447
- const key = el.dataset.ioKey;
1448
- if (!key)
1449
- return;
1450
- if (el.open)
1451
- openBlocks.add(key);
1452
- else
1453
- openBlocks.delete(key);
1454
- const pre = el.querySelector('.wf-io-pre');
1455
- if (pre)
1456
- scrollTops.set(key, pre.scrollTop);
1457
- });
1458
- }
1459
- function attachIOBlockToggleTracking(root, openBlocks) {
1460
- root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
1461
- if (el.dataset.ioToggleBound === '1')
1462
- return;
1463
- el.dataset.ioToggleBound = '1';
1464
- el.addEventListener('toggle', () => {
1465
- const key = el.dataset.ioKey;
1466
- if (!key)
1467
- return;
1468
- if (el.open)
1469
- openBlocks.add(key);
1470
- else
1471
- openBlocks.delete(key);
1472
- });
1473
- });
1474
- }
1475
- function restoreIOBlockScroll(root, scrollTops) {
1476
- root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
1477
- const key = el.dataset.ioKey;
1478
- if (!key)
1479
- return;
1480
- const top = scrollTops.get(key);
1481
- if (top === undefined)
1482
- return;
1483
- const pre = el.querySelector('.wf-io-pre');
1484
- if (pre)
1485
- pre.scrollTop = top;
1486
- });
1487
- }
1488
- function attachIOBlockScrollTracking(root, scrollTops) {
1489
- root.querySelectorAll('details.wf-io-block[data-io-key]').forEach((el) => {
1490
- const key = el.dataset.ioKey;
1491
- if (!key)
1492
- return;
1493
- const pre = el.querySelector('.wf-io-pre');
1494
- if (!pre)
1495
- return;
1496
- if (pre.dataset.ioScrollBound === '1')
1497
- return;
1498
- pre.dataset.ioScrollBound = '1';
1499
- pre.addEventListener('scroll', () => {
1500
- scrollTops.set(key, pre.scrollTop);
1501
- });
1502
- });
1503
- }
1504
- function previewMeta(preview) {
353
+ export function previewMetaParts(preview) {
1505
354
  if (!preview)
1506
- return `<span class="muted">${escapeHtml(t('workflow.detail.empty'))}</span>`;
355
+ return [t('workflow.detail.empty')];
1507
356
  const bits = [];
1508
357
  if (preview.outputBytes !== undefined)
1509
358
  bits.push(`${preview.outputBytes}B`);
@@ -1513,73 +362,23 @@ function previewMeta(preview) {
1513
362
  bits.push(t('workflow.detail.error'));
1514
363
  if (preview.outputHash)
1515
364
  bits.push(short(preview.outputHash));
1516
- return bits.length ? `<span class="muted">${escapeHtml(bits.join(' · '))}</span>` : '';
365
+ return bits;
1517
366
  }
1518
- function renderPreview(preview) {
367
+ export function previewBody(preview) {
1519
368
  if (!preview)
1520
- return `<div class="muted wf-io-empty">${escapeHtml(t('workflow.detail.noData'))}</div>`;
1521
- const body = preview.value !== undefined
369
+ return '';
370
+ return preview.value !== undefined
1522
371
  ? JSON.stringify(preview.value, null, 2)
1523
372
  : preview.text ?? '';
1524
- const error = preview.error ? `<div class="muted error">${escapeHtml(preview.error)}</div>` : '';
1525
- if (!body)
1526
- return `${error}<div class="muted wf-io-empty">${escapeHtml(t('workflow.detail.noPreview'))}</div>`;
1527
- return `${error}<pre class="wf-io-pre">${escapeHtml(body)}</pre>`;
1528
- }
1529
- function renderAttemptDetail(at) {
1530
- const parts = [];
1531
- if (at.effectAttempted)
1532
- parts.push(`${escapeHtml(t('workflow.detail.effect'))} ${escapeHtml(at.effectAttempted.provider)}`);
1533
- if (at.wait) {
1534
- const res = at.wait.resolution
1535
- ? `${at.wait.resolution.kind}${at.wait.resolution.resolution ? ':' + at.wait.resolution.resolution : ''}`
1536
- : t('workflow.detail.open');
1537
- parts.push(`${escapeHtml(t('workflow.detail.wait'))} ${escapeHtml(at.wait.waitKind)} ${escapeHtml(res)}`);
1538
- if (at.wait.deadlineAt !== undefined) {
1539
- parts.push(`${escapeHtml(t('workflow.detail.deadline'))} ${escapeHtml(formatClock(at.wait.deadlineAt))}`);
1540
- }
1541
- }
1542
- if (at.error) {
1543
- const tag = `${at.error.errorCode}${at.error.errorClass ? ` · ${at.error.errorClass}` : ''}`;
1544
- parts.push(`<span class="muted error">${escapeHtml(tag)}</span>`);
1545
- if (at.error.errorMessage) {
1546
- parts.push(`<span class="error wf-error-msg">${escapeHtml(at.error.errorMessage)}</span>`);
1547
- }
1548
- }
1549
- if (at.output)
1550
- parts.push(`${escapeHtml(t('workflow.detail.output'))} ${escapeHtml(short(at.output.outputHash))}`);
1551
- if (at.runningMs !== undefined)
1552
- parts.push(`${at.runningMs}ms`);
1553
- return parts.length > 0 ? parts.join('<br/>') : '<span class="muted">-</span>';
1554
- }
1555
- function renderEvents(tbody, events) {
1556
- tbody.innerHTML =
1557
- events.length > 0
1558
- ? events.map(renderEventRow).join('')
1559
- : `<tr><td colspan="7" class="empty">${escapeHtml(t('workflow.detail.noEvents'))}</td></tr>`;
1560
- }
1561
- function renderEventRow(ev) {
1562
- const ctx = extractEventContext(ev.payload);
1563
- return `<tr>
1564
- <td>${eventSeqFromId(ev.eventId)}</td>
1565
- <td><code>${escapeHtml(ev.type)}</code></td>
1566
- <td>${escapeHtml(ev.actor)}</td>
1567
- <td>${ctx.nodeId ? `<code>${escapeHtml(ctx.nodeId)}</code>` : '-'}</td>
1568
- <td>${ctx.activityId ? `<code>${escapeHtml(ctx.activityId)}</code>` : '-'}</td>
1569
- <td>${ctx.errorCode ? `<span class="muted error">${escapeHtml(ctx.errorCode)}</span>` : '-'}</td>
1570
- <td title="${escapeHtml(new Date(ev.timestamp).toISOString())}">${escapeHtml(formatClock(ev.timestamp))}</td>
1571
- </tr>`;
1572
373
  }
1573
- // Browser-side copies of ops-projection helpers. Keep these tiny to avoid
1574
- // pulling the Node/Zod projection module into the dashboard bundle.
1575
- function eventSeqFromId(eventId) {
374
+ export function eventSeqFromId(eventId) {
1576
375
  const dash = eventId.lastIndexOf('-');
1577
376
  if (dash < 0)
1578
377
  return 0;
1579
378
  const n = Number(eventId.slice(dash + 1));
1580
379
  return Number.isFinite(n) ? n : 0;
1581
380
  }
1582
- function extractEventContext(payload) {
381
+ export function extractEventContext(payload) {
1583
382
  if (!payload || typeof payload !== 'object' || 'ref' in payload)
1584
383
  return {};
1585
384
  const p = payload;
@@ -1596,27 +395,42 @@ function extractEventContext(payload) {
1596
395
  }
1597
396
  return out;
1598
397
  }
1599
- function payloadRecord(ev) {
1600
- if (!ev.payload || typeof ev.payload !== 'object' || 'ref' in ev.payload)
1601
- return null;
1602
- return ev.payload;
1603
- }
1604
- function clamp(value, min, max) {
398
+ export function clamp(value, min, max) {
1605
399
  return Math.min(max, Math.max(min, value));
1606
400
  }
1607
- function short(value) {
401
+ export function short(value) {
1608
402
  if (!value)
1609
403
  return '-';
1610
404
  return value.length > 18 ? value.slice(0, 10) + '...' + value.slice(-6) : value;
1611
405
  }
1612
- function shortText(value, max) {
406
+ export function shortText(value, max) {
1613
407
  return value.length > max ? value.slice(0, max - 1) + '…' : value;
1614
408
  }
1615
- function formatClock(ms) {
409
+ export function formatClock(ms) {
1616
410
  return new Date(ms).toLocaleTimeString([], {
1617
411
  hour: '2-digit',
1618
412
  minute: '2-digit',
1619
413
  second: '2-digit',
1620
414
  });
1621
415
  }
416
+ function payloadRecord(event) {
417
+ if (!event.payload || typeof event.payload !== 'object' || 'ref' in event.payload)
418
+ return null;
419
+ return event.payload;
420
+ }
421
+ function isTerminalActivityEvent(type) {
422
+ return type === 'activitySucceeded' ||
423
+ type === 'activityFailed' ||
424
+ type === 'activityTimedOut' ||
425
+ type === 'activityCanceled';
426
+ }
427
+ function terminalStatusForEvent(type) {
428
+ if (type === 'activitySucceeded')
429
+ return 'succeeded';
430
+ if (type === 'activityCanceled')
431
+ return 'cancelled';
432
+ if (type === 'activityTimedOut')
433
+ return 'timedOut';
434
+ return 'failed';
435
+ }
1622
436
  //# sourceMappingURL=workflows.js.map