openclaw-multi-auto 1.3.6 → 1.3.8

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 (328) hide show
  1. package/dist/{audio-preflight-5FEeDooz.js → audio-preflight-DDBLZBdb.js} +4 -4
  2. package/dist/{audio-transcription-runner-B-UvoDjZ.js → audio-transcription-runner-DZbSWT9E.js} +1 -1
  3. package/dist/build-info.json +3 -3
  4. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  5. package/dist/{chrome-D45SyhQL.js → chrome-CMU2WVFh.js} +8 -8
  6. package/dist/{deliver-B9cys0EZ.js → deliver-BXVcFIHL.js} +1 -1
  7. package/dist/{deliver-runtime-DhaQJ0pI.js → deliver-runtime-DTaIS-1i.js} +3 -3
  8. package/dist/{deps-send-whatsapp.runtime-DvTL2tzN.js → deps-send-whatsapp.runtime-CIZqFAqb.js} +7 -7
  9. package/dist/extensionAPI.js +6 -6
  10. package/dist/{image-DAOPwVXi.js → image-BCVLo0qw.js} +1 -1
  11. package/dist/{image-runtime-wlCLVvVv.js → image-runtime-DtCKpMPZ.js} +3 -3
  12. package/dist/{pi-embedded-DYU79yGe.js → pi-embedded-CgQ_W6Xs.js} +24 -24
  13. package/dist/{pi-embedded-helpers-uTRAmQ4n.js → pi-embedded-helpers-CwuBTKza.js} +3 -3
  14. package/dist/plugin-sdk/{accounts-DyFCXtHv.js → accounts-BslAlVYS.js} +2 -2
  15. package/dist/plugin-sdk/{accounts-BJAXxY46.js → accounts-C3m65--E.js} +2 -2
  16. package/dist/plugin-sdk/{accounts-C1j7HSL0.js → accounts-CNCCkdEF.js} +3 -3
  17. package/dist/plugin-sdk/{active-listener-CftX5jLD.js → active-listener-CkPnMUkB.js} +2 -2
  18. package/dist/plugin-sdk/{api-key-rotation-8nyyt1kx.js → api-key-rotation-BXnNsojA.js} +2 -2
  19. package/dist/plugin-sdk/{audio-preflight-C_aSAPR1.js → audio-preflight-CtO4fFvp.js} +26 -26
  20. package/dist/plugin-sdk/{audio-transcription-runner-CB53F7_7.js → audio-transcription-runner-DnxvOS1-.js} +11 -11
  21. package/dist/plugin-sdk/{audit-membership-runtime-BXndI4LG.js → audit-membership-runtime-BpfoSk8M.js} +2 -2
  22. package/dist/plugin-sdk/{channel-activity-C5y8AgAV.js → channel-activity-WJYxcJ3S.js} +3 -3
  23. package/dist/plugin-sdk/{channel-web-DBTRO03V.js → channel-web-dO5k3ubM.js} +18 -18
  24. package/dist/plugin-sdk/{chrome-f00sZkDX.js → chrome-CjNTuJML.js} +6 -6
  25. package/dist/plugin-sdk/{commands-registry-BJ_NxG2F.js → commands-registry-CdYjoI0i.js} +4 -4
  26. package/dist/plugin-sdk/{common-Cf27Jwxu.js → common-oYc5vPFl.js} +2 -2
  27. package/dist/plugin-sdk/{config-CHQrpx-Q.js → config-B1z-UxQ3.js} +7 -7
  28. package/dist/plugin-sdk/{deliver-DNEuetST.js → deliver-D5_6T567.js} +10 -10
  29. package/dist/plugin-sdk/deliver-runtime-C5dgvvga.js +32 -0
  30. package/dist/plugin-sdk/deps-send-discord.runtime-Dg4N7PHJ.js +23 -0
  31. package/dist/plugin-sdk/deps-send-imessage.runtime-0OEwzMQm.js +22 -0
  32. package/dist/plugin-sdk/deps-send-signal.runtime-BM1jRt3G.js +21 -0
  33. package/dist/plugin-sdk/deps-send-slack.runtime-1E3BYRdF.js +19 -0
  34. package/dist/plugin-sdk/deps-send-telegram.runtime-DNCxIflA.js +24 -0
  35. package/dist/plugin-sdk/deps-send-whatsapp.runtime-OLwr-9c8.js +57 -0
  36. package/dist/plugin-sdk/{diagnostic-LYUUmjJ5.js → diagnostic-Bxxu0ig-.js} +2 -2
  37. package/dist/plugin-sdk/{errors-CtMWwS2Z.js → errors-B3cHyZZA.js} +1 -1
  38. package/dist/plugin-sdk/{fetch-guard-CxYB5Kg6.js → fetch-guard-Dcgod0tg.js} +2 -2
  39. package/dist/plugin-sdk/{fs-safe-DtfhxbrI.js → fs-safe-BaKqI3G4.js} +3 -3
  40. package/dist/plugin-sdk/{image-BwjYjRHx.js → image-B2mQW9Rb.js} +6 -6
  41. package/dist/plugin-sdk/{image-ops-BnZKcbd6.js → image-ops-Cbzr4U9l.js} +2 -2
  42. package/dist/plugin-sdk/image-runtime-BFm45j49.js +25 -0
  43. package/dist/plugin-sdk/{ir-Z4hX67TJ.js → ir-ZEmrTr4J.js} +7 -7
  44. package/dist/plugin-sdk/{local-roots-KhjQw04O.js → local-roots-CIPRxA-4.js} +4 -4
  45. package/dist/plugin-sdk/{logger-DHIIvMxj.js → logger-CvPFVOgT.js} +2 -2
  46. package/dist/plugin-sdk/{login-C31642Ld.js → login-CCTew9bt.js} +4 -4
  47. package/dist/plugin-sdk/{login-qr--y2SG_Ue.js → login-qr-BI3Vi_wJ.js} +5 -5
  48. package/dist/plugin-sdk/{manager-2UZBMCc7.js → manager-BEoYPn7R.js} +8 -8
  49. package/dist/plugin-sdk/manager-runtime-DxclHQ4U.js +15 -0
  50. package/dist/plugin-sdk/{outbound-Ba0QUI5h.js → outbound-ByOw1K6W.js} +5 -5
  51. package/dist/plugin-sdk/{outbound-attachment-B1Laso-8.js → outbound-attachment-BzVhxRRw.js} +2 -2
  52. package/dist/plugin-sdk/{path-alias-guards-C7Vm5DZ1.js → path-alias-guards-sWayacde.js} +1 -1
  53. package/dist/plugin-sdk/{paths-DopV9PQG.js → paths-Dpg3qxcl.js} +1 -1
  54. package/dist/plugin-sdk/{pi-embedded-helpers-DnA_OCzP.js → pi-embedded-helpers-DIxXkGJf.js} +16 -16
  55. package/dist/plugin-sdk/{pi-model-discovery-DdPqXk8f.js → pi-model-discovery-DM_2uFtj.js} +1 -1
  56. package/dist/plugin-sdk/pi-model-discovery-runtime-BuzvkvNR.js +8 -0
  57. package/dist/plugin-sdk/{pi-tools.before-tool-call.runtime-DxFHiLUE.js → pi-tools.before-tool-call.runtime-w1dqL_ty.js} +4 -4
  58. package/dist/plugin-sdk/{plugins-CbCt4osF.js → plugins-C4USiH29.js} +4 -4
  59. package/dist/plugin-sdk/{proxy-env-C63mMdas.js → proxy-env-ET-rp8eg.js} +1 -1
  60. package/dist/plugin-sdk/{proxy-fetch-Ch95c_Y2.js → proxy-fetch-uDXGKG3Z.js} +1 -1
  61. package/dist/plugin-sdk/{pw-ai-DpJk62D4.js → pw-ai-CyOt3RDA.js} +9 -9
  62. package/dist/plugin-sdk/{qmd-manager-Ca-iSfEE.js → qmd-manager-BySdoVR7.js} +7 -7
  63. package/dist/plugin-sdk/{query-expansion-B_Xe41Ab.js → query-expansion-C6uS-7lj.js} +4 -4
  64. package/dist/plugin-sdk/{redact-hp9TOulW.js → redact-Bvxt1T_Q.js} +1 -1
  65. package/dist/plugin-sdk/{reply-CovBlFea.js → reply-CTCSeQqW.js} +73 -73
  66. package/dist/plugin-sdk/{resolve-outbound-target-BbrHgyUk.js → resolve-outbound-target-Bw8YNANu.js} +2 -2
  67. package/dist/plugin-sdk/{run-with-concurrency-BR1DXa8T.js → run-with-concurrency-C_KCHwvf.js} +1 -1
  68. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-BxgRDkhc.js +10 -0
  69. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-elOqrkfg.js +19 -0
  70. package/dist/plugin-sdk/{send-BvAtLLPl.js → send-BZ6nYFZr.js} +5 -5
  71. package/dist/plugin-sdk/{send-BTztm3D2.js → send-C0w6xP2x.js} +6 -6
  72. package/dist/plugin-sdk/{send-CWJUuG0i.js → send-CFf-1V89.js} +8 -8
  73. package/dist/plugin-sdk/{send-EcglC4cG.js → send-CY-Qfwia.js} +7 -7
  74. package/dist/plugin-sdk/{send-BXpXBwM_.js → send-qPyNGSe4.js} +13 -13
  75. package/dist/plugin-sdk/{session-k256LJZT.js → session-COrvpvUQ.js} +3 -3
  76. package/dist/plugin-sdk/signal.js +2 -2
  77. package/dist/plugin-sdk/{skill-commands-DoRqLzxm.js → skill-commands-DZqhtmiv.js} +4 -4
  78. package/dist/plugin-sdk/{skills-QudILG6e.js → skills-Cw_vXEJb.js} +6 -6
  79. package/dist/plugin-sdk/slash-commands.runtime-D67JLweo.js +13 -0
  80. package/dist/plugin-sdk/slash-dispatch.runtime-DvcpvCJ0.js +52 -0
  81. package/dist/plugin-sdk/slash-skill-commands.runtime-BM1x3azR.js +16 -0
  82. package/dist/plugin-sdk/{store-BbDQw3g6.js → store-CMHj6IIw.js} +2 -2
  83. package/dist/plugin-sdk/subagent-registry-runtime-1lbDyRzz.js +52 -0
  84. package/dist/plugin-sdk/{tables-BhvloMKN.js → tables-CSqrHsKL.js} +1 -1
  85. package/dist/plugin-sdk/{thinking-URzkT-3p.js → thinking-DOnsR_A8.js} +7 -7
  86. package/dist/plugin-sdk/{tokens-B1PW5Ayy.js → tokens-BDr0Z9o3.js} +1 -1
  87. package/dist/plugin-sdk/{tool-images-xpqbP6RR.js → tool-images-eEfOVkzf.js} +2 -2
  88. package/dist/plugin-sdk/web-BLyT64pW.js +56 -0
  89. package/dist/plugin-sdk/{whatsapp-actions-RcZ6vp61.js → whatsapp-actions-xcleMoMv.js} +17 -17
  90. package/dist/plugin-sdk/whatsapp.js +50 -50
  91. package/dist/{pw-ai-GcYO6HPE.js → pw-ai-CmphSzHx.js} +1 -1
  92. package/dist/{slash-dispatch.runtime-Dh053pQK.js → slash-dispatch.runtime-131yup2e.js} +6 -6
  93. package/dist/{subagent-registry-runtime-DSi5mnCQ.js → subagent-registry-runtime-DbSf_Je6.js} +6 -6
  94. package/dist/{web-1hWJDzNA.js → web-MR9d7KyB.js} +6 -6
  95. package/package.json +5 -2
  96. package/scripts/create-instance.sh +44 -19
  97. package/scripts/install-maca.sh +39 -28
  98. package/scripts/npm_publish.sh +8 -6
  99. package/ui/index.html +16 -0
  100. package/ui/node_modules/.bin/jiti +21 -0
  101. package/ui/node_modules/.bin/lessc +21 -0
  102. package/ui/node_modules/.bin/marked +21 -0
  103. package/ui/node_modules/.bin/playwright +21 -0
  104. package/ui/node_modules/.bin/sass +21 -0
  105. package/ui/node_modules/.bin/tsx +21 -0
  106. package/ui/node_modules/.bin/vite +21 -0
  107. package/ui/node_modules/.bin/vitest +21 -0
  108. package/ui/node_modules/.bin/yaml +21 -0
  109. package/ui/package.json +27 -0
  110. package/ui/public/apple-touch-icon.png +0 -0
  111. package/ui/public/favicon-32.png +0 -0
  112. package/ui/public/favicon.ico +0 -0
  113. package/ui/public/favicon.svg +22 -0
  114. package/ui/src/css.d.ts +1 -0
  115. package/ui/src/i18n/index.ts +3 -0
  116. package/ui/src/i18n/lib/lit-controller.ts +22 -0
  117. package/ui/src/i18n/lib/registry.ts +64 -0
  118. package/ui/src/i18n/lib/translate.ts +123 -0
  119. package/ui/src/i18n/lib/types.ts +9 -0
  120. package/ui/src/i18n/locales/de.ts +129 -0
  121. package/ui/src/i18n/locales/en.ts +337 -0
  122. package/ui/src/i18n/locales/pt-BR.ts +128 -0
  123. package/ui/src/i18n/locales/zh-CN.ts +330 -0
  124. package/ui/src/i18n/locales/zh-TW.ts +125 -0
  125. package/ui/src/i18n/test/translate.test.ts +56 -0
  126. package/ui/src/main.ts +2 -0
  127. package/ui/src/styles/base.css +385 -0
  128. package/ui/src/styles/chat/grouped.css +300 -0
  129. package/ui/src/styles/chat/layout.css +481 -0
  130. package/ui/src/styles/chat/sidebar.css +117 -0
  131. package/ui/src/styles/chat/text.css +146 -0
  132. package/ui/src/styles/chat/tool-cards.css +202 -0
  133. package/ui/src/styles/chat.css +5 -0
  134. package/ui/src/styles/components.css +2612 -0
  135. package/ui/src/styles/config.css +1658 -0
  136. package/ui/src/styles/layout.css +621 -0
  137. package/ui/src/styles/layout.mobile.css +374 -0
  138. package/ui/src/styles.css +5 -0
  139. package/ui/src/ui/__screenshots__/config-form.browser.test.ts/config-form-renderer-flags-unsupported-unions-1.png +0 -0
  140. package/ui/src/ui/__screenshots__/config-form.browser.test.ts/config-form-renderer-renders-inputs-and-patches-values-1.png +0 -0
  141. package/ui/src/ui/__screenshots__/config-form.browser.test.ts/config-form-renderer-renders-union-literals-as-select-options-1.png +0 -0
  142. package/ui/src/ui/__screenshots__/navigation.browser.test.ts/control-UI-routing-auto-scrolls-chat-history-to-the-latest-message-1.png +0 -0
  143. package/ui/src/ui/app-channels.ts +279 -0
  144. package/ui/src/ui/app-chat.ts +266 -0
  145. package/ui/src/ui/app-defaults.ts +50 -0
  146. package/ui/src/ui/app-events.ts +5 -0
  147. package/ui/src/ui/app-gateway.node.test.ts +229 -0
  148. package/ui/src/ui/app-gateway.ts +349 -0
  149. package/ui/src/ui/app-lifecycle.node.test.ts +44 -0
  150. package/ui/src/ui/app-lifecycle.ts +109 -0
  151. package/ui/src/ui/app-polling.ts +69 -0
  152. package/ui/src/ui/app-render-usage-tab.ts +273 -0
  153. package/ui/src/ui/app-render.helpers.node.test.ts +286 -0
  154. package/ui/src/ui/app-render.helpers.ts +574 -0
  155. package/ui/src/ui/app-render.ts +1168 -0
  156. package/ui/src/ui/app-scroll.test.ts +275 -0
  157. package/ui/src/ui/app-scroll.ts +179 -0
  158. package/ui/src/ui/app-settings.test.ts +70 -0
  159. package/ui/src/ui/app-settings.ts +440 -0
  160. package/ui/src/ui/app-tool-stream.node.test.ts +139 -0
  161. package/ui/src/ui/app-tool-stream.ts +455 -0
  162. package/ui/src/ui/app-view-state.ts +321 -0
  163. package/ui/src/ui/app.ts +621 -0
  164. package/ui/src/ui/assistant-identity.ts +23 -0
  165. package/ui/src/ui/chat/constants.ts +12 -0
  166. package/ui/src/ui/chat/copy-as-markdown.ts +97 -0
  167. package/ui/src/ui/chat/grouped-render.ts +287 -0
  168. package/ui/src/ui/chat/message-extract.test.ts +64 -0
  169. package/ui/src/ui/chat/message-extract.ts +122 -0
  170. package/ui/src/ui/chat/message-normalizer.test.ts +179 -0
  171. package/ui/src/ui/chat/message-normalizer.ts +101 -0
  172. package/ui/src/ui/chat/tool-cards.ts +156 -0
  173. package/ui/src/ui/chat/tool-helpers.test.ts +141 -0
  174. package/ui/src/ui/chat/tool-helpers.ts +37 -0
  175. package/ui/src/ui/chat-event-reload.test.ts +47 -0
  176. package/ui/src/ui/chat-event-reload.ts +16 -0
  177. package/ui/src/ui/chat-markdown.browser.test.ts +37 -0
  178. package/ui/src/ui/components/resizable-divider.ts +110 -0
  179. package/ui/src/ui/config-form.browser.test.ts +443 -0
  180. package/ui/src/ui/controllers/agent-files.ts +126 -0
  181. package/ui/src/ui/controllers/agent-identity.ts +59 -0
  182. package/ui/src/ui/controllers/agent-skills.ts +33 -0
  183. package/ui/src/ui/controllers/agents.test.ts +61 -0
  184. package/ui/src/ui/controllers/agents.ts +64 -0
  185. package/ui/src/ui/controllers/assistant-identity.ts +34 -0
  186. package/ui/src/ui/controllers/channels.ts +94 -0
  187. package/ui/src/ui/controllers/channels.types.ts +15 -0
  188. package/ui/src/ui/controllers/chat.test.ts +568 -0
  189. package/ui/src/ui/controllers/chat.ts +318 -0
  190. package/ui/src/ui/controllers/config/form-coerce.ts +160 -0
  191. package/ui/src/ui/controllers/config/form-utils.node.test.ts +455 -0
  192. package/ui/src/ui/controllers/config/form-utils.ts +90 -0
  193. package/ui/src/ui/controllers/config.test.ts +289 -0
  194. package/ui/src/ui/controllers/config.ts +219 -0
  195. package/ui/src/ui/controllers/control-ui-bootstrap.test.ts +82 -0
  196. package/ui/src/ui/controllers/control-ui-bootstrap.ts +49 -0
  197. package/ui/src/ui/controllers/cron-filters.test.ts +81 -0
  198. package/ui/src/ui/controllers/cron.test.ts +1070 -0
  199. package/ui/src/ui/controllers/cron.ts +921 -0
  200. package/ui/src/ui/controllers/debug.ts +60 -0
  201. package/ui/src/ui/controllers/devices.ts +159 -0
  202. package/ui/src/ui/controllers/exec-approval.ts +100 -0
  203. package/ui/src/ui/controllers/exec-approvals.ts +170 -0
  204. package/ui/src/ui/controllers/logs.ts +147 -0
  205. package/ui/src/ui/controllers/nodes.ts +32 -0
  206. package/ui/src/ui/controllers/presence.ts +37 -0
  207. package/ui/src/ui/controllers/sessions.test.ts +104 -0
  208. package/ui/src/ui/controllers/sessions.ts +127 -0
  209. package/ui/src/ui/controllers/skills.ts +157 -0
  210. package/ui/src/ui/controllers/usage.node.test.ts +181 -0
  211. package/ui/src/ui/controllers/usage.ts +315 -0
  212. package/ui/src/ui/data/moonshot-kimi-k2.ts +45 -0
  213. package/ui/src/ui/device-auth.ts +73 -0
  214. package/ui/src/ui/device-identity.ts +112 -0
  215. package/ui/src/ui/external-link.test.ts +18 -0
  216. package/ui/src/ui/external-link.ts +19 -0
  217. package/ui/src/ui/focus-mode.browser.test.ts +39 -0
  218. package/ui/src/ui/format.test.ts +101 -0
  219. package/ui/src/ui/format.ts +60 -0
  220. package/ui/src/ui/gateway.ts +360 -0
  221. package/ui/src/ui/icons.ts +256 -0
  222. package/ui/src/ui/markdown.test.ts +85 -0
  223. package/ui/src/ui/markdown.ts +139 -0
  224. package/ui/src/ui/navigation.browser.test.ts +188 -0
  225. package/ui/src/ui/navigation.test.ts +189 -0
  226. package/ui/src/ui/navigation.ts +165 -0
  227. package/ui/src/ui/open-external-url.test.ts +108 -0
  228. package/ui/src/ui/open-external-url.ts +73 -0
  229. package/ui/src/ui/presenter.ts +85 -0
  230. package/ui/src/ui/storage.node.test.ts +63 -0
  231. package/ui/src/ui/storage.ts +99 -0
  232. package/ui/src/ui/test-helpers/app-mount.ts +27 -0
  233. package/ui/src/ui/text-direction.test.ts +24 -0
  234. package/ui/src/ui/text-direction.ts +30 -0
  235. package/ui/src/ui/theme-transition.ts +109 -0
  236. package/ui/src/ui/theme.ts +16 -0
  237. package/ui/src/ui/tool-display.ts +159 -0
  238. package/ui/src/ui/types/chat-types.ts +44 -0
  239. package/ui/src/ui/types.ts +627 -0
  240. package/ui/src/ui/ui-types.ts +54 -0
  241. package/ui/src/ui/usage-helpers.node.test.ts +43 -0
  242. package/ui/src/ui/usage-helpers.ts +321 -0
  243. package/ui/src/ui/usage-types.ts +22 -0
  244. package/ui/src/ui/uuid.test.ts +41 -0
  245. package/ui/src/ui/uuid.ts +57 -0
  246. package/ui/src/ui/views/agents-panels-status-files.ts +461 -0
  247. package/ui/src/ui/views/agents-panels-tools-skills.browser.test.ts +102 -0
  248. package/ui/src/ui/views/agents-panels-tools-skills.ts +537 -0
  249. package/ui/src/ui/views/agents-utils.test.ts +100 -0
  250. package/ui/src/ui/views/agents-utils.ts +502 -0
  251. package/ui/src/ui/views/agents.ts +499 -0
  252. package/ui/src/ui/views/channel-config-extras.ts +49 -0
  253. package/ui/src/ui/views/channels.config.ts +155 -0
  254. package/ui/src/ui/views/channels.discord.ts +65 -0
  255. package/ui/src/ui/views/channels.googlechat.ts +79 -0
  256. package/ui/src/ui/views/channels.imessage.ts +65 -0
  257. package/ui/src/ui/views/channels.nostr-profile-form.ts +321 -0
  258. package/ui/src/ui/views/channels.nostr.ts +237 -0
  259. package/ui/src/ui/views/channels.shared.ts +38 -0
  260. package/ui/src/ui/views/channels.signal.ts +69 -0
  261. package/ui/src/ui/views/channels.slack.ts +65 -0
  262. package/ui/src/ui/views/channels.telegram.ts +120 -0
  263. package/ui/src/ui/views/channels.ts +325 -0
  264. package/ui/src/ui/views/channels.types.ts +62 -0
  265. package/ui/src/ui/views/channels.whatsapp.ts +118 -0
  266. package/ui/src/ui/views/chat-image-open.browser.test.ts +70 -0
  267. package/ui/src/ui/views/chat.test.ts +227 -0
  268. package/ui/src/ui/views/chat.ts +616 -0
  269. package/ui/src/ui/views/config-form.analyze.ts +267 -0
  270. package/ui/src/ui/views/config-form.node.ts +1073 -0
  271. package/ui/src/ui/views/config-form.render.ts +478 -0
  272. package/ui/src/ui/views/config-form.search.node.test.ts +69 -0
  273. package/ui/src/ui/views/config-form.shared.ts +96 -0
  274. package/ui/src/ui/views/config-form.ts +4 -0
  275. package/ui/src/ui/views/config-search.node.test.ts +50 -0
  276. package/ui/src/ui/views/config-search.ts +92 -0
  277. package/ui/src/ui/views/config.browser.test.ts +233 -0
  278. package/ui/src/ui/views/config.ts +820 -0
  279. package/ui/src/ui/views/cron.test.ts +741 -0
  280. package/ui/src/ui/views/cron.ts +1758 -0
  281. package/ui/src/ui/views/debug.ts +151 -0
  282. package/ui/src/ui/views/exec-approval.ts +89 -0
  283. package/ui/src/ui/views/gateway-url-confirmation.ts +40 -0
  284. package/ui/src/ui/views/instances.ts +89 -0
  285. package/ui/src/ui/views/logs.ts +155 -0
  286. package/ui/src/ui/views/markdown-sidebar.ts +40 -0
  287. package/ui/src/ui/views/nodes-exec-approvals.ts +617 -0
  288. package/ui/src/ui/views/nodes-shared.ts +67 -0
  289. package/ui/src/ui/views/nodes.ts +485 -0
  290. package/ui/src/ui/views/overview-hints.ts +16 -0
  291. package/ui/src/ui/views/overview.node.test.ts +39 -0
  292. package/ui/src/ui/views/overview.ts +361 -0
  293. package/ui/src/ui/views/sessions.test.ts +81 -0
  294. package/ui/src/ui/views/sessions.ts +321 -0
  295. package/ui/src/ui/views/skills-grouping.ts +40 -0
  296. package/ui/src/ui/views/skills-shared.ts +52 -0
  297. package/ui/src/ui/views/skills.ts +192 -0
  298. package/ui/src/ui/views/usage-metrics.ts +578 -0
  299. package/ui/src/ui/views/usage-query.ts +277 -0
  300. package/ui/src/ui/views/usage-render-details.test.ts +136 -0
  301. package/ui/src/ui/views/usage-render-details.ts +1083 -0
  302. package/ui/src/ui/views/usage-render-overview.ts +796 -0
  303. package/ui/src/ui/views/usage-styles/usageStyles-part1.ts +701 -0
  304. package/ui/src/ui/views/usage-styles/usageStyles-part2.ts +702 -0
  305. package/ui/src/ui/views/usage-styles/usageStyles-part3.ts +551 -0
  306. package/ui/src/ui/views/usage.ts +836 -0
  307. package/ui/src/ui/views/usageStyles.ts +5 -0
  308. package/ui/src/ui/views/usageTypes.ts +105 -0
  309. package/ui/vite.config.ts +43 -0
  310. package/ui/vitest.config.ts +15 -0
  311. package/ui/vitest.node.config.ts +10 -0
  312. package/dist/plugin-sdk/deliver-runtime-BFdqklJM.js +0 -32
  313. package/dist/plugin-sdk/deps-send-discord.runtime-DuqpYwU0.js +0 -23
  314. package/dist/plugin-sdk/deps-send-imessage.runtime-CZ2rS8Lb.js +0 -22
  315. package/dist/plugin-sdk/deps-send-signal.runtime-BdqiWhIh.js +0 -21
  316. package/dist/plugin-sdk/deps-send-slack.runtime-04s36qiC.js +0 -19
  317. package/dist/plugin-sdk/deps-send-telegram.runtime-LE5tkPvr.js +0 -24
  318. package/dist/plugin-sdk/deps-send-whatsapp.runtime-Bz57lobC.js +0 -57
  319. package/dist/plugin-sdk/image-runtime-B8twoubs.js +0 -25
  320. package/dist/plugin-sdk/manager-runtime-CMeLwose.js +0 -15
  321. package/dist/plugin-sdk/pi-model-discovery-runtime-D8CJhtJY.js +0 -8
  322. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-SkO91TZH.js +0 -10
  323. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-B0VWK5hm.js +0 -19
  324. package/dist/plugin-sdk/slash-commands.runtime-DS6vCNSL.js +0 -13
  325. package/dist/plugin-sdk/slash-dispatch.runtime-BXrxb2wd.js +0 -52
  326. package/dist/plugin-sdk/slash-skill-commands.runtime-Bd6qQ2oT.js +0 -16
  327. package/dist/plugin-sdk/subagent-registry-runtime-1uwQbuXj.js +0 -52
  328. package/dist/plugin-sdk/web-B74yhL2N.js +0 -56
@@ -0,0 +1,1758 @@
1
+ import { html, nothing } from "lit";
2
+ import { ifDefined } from "lit/directives/if-defined.js";
3
+ import { t } from "../../i18n/index.ts";
4
+ import type {
5
+ CronFieldErrors,
6
+ CronFieldKey,
7
+ CronJobsLastStatusFilter,
8
+ CronJobsScheduleKindFilter,
9
+ } from "../controllers/cron.ts";
10
+ import { formatRelativeTimestamp, formatMs } from "../format.ts";
11
+ import { pathForTab } from "../navigation.ts";
12
+ import { formatCronSchedule, formatNextRun } from "../presenter.ts";
13
+ import type { ChannelUiMetaEntry, CronJob, CronRunLogEntry, CronStatus } from "../types.ts";
14
+ import type {
15
+ CronDeliveryStatus,
16
+ CronJobsEnabledFilter,
17
+ CronRunScope,
18
+ CronRunsStatusValue,
19
+ CronJobsSortBy,
20
+ CronRunsStatusFilter,
21
+ CronSortDir,
22
+ } from "../types.ts";
23
+ import type { CronFormState } from "../ui-types.ts";
24
+
25
+ export type CronProps = {
26
+ basePath: string;
27
+ loading: boolean;
28
+ jobsLoadingMore: boolean;
29
+ status: CronStatus | null;
30
+ jobs: CronJob[];
31
+ jobsTotal: number;
32
+ jobsHasMore: boolean;
33
+ jobsQuery: string;
34
+ jobsEnabledFilter: CronJobsEnabledFilter;
35
+ jobsScheduleKindFilter: CronJobsScheduleKindFilter;
36
+ jobsLastStatusFilter: CronJobsLastStatusFilter;
37
+ jobsSortBy: CronJobsSortBy;
38
+ jobsSortDir: CronSortDir;
39
+ error: string | null;
40
+ busy: boolean;
41
+ form: CronFormState;
42
+ fieldErrors: CronFieldErrors;
43
+ canSubmit: boolean;
44
+ editingJobId: string | null;
45
+ channels: string[];
46
+ channelLabels?: Record<string, string>;
47
+ channelMeta?: ChannelUiMetaEntry[];
48
+ runsJobId: string | null;
49
+ runs: CronRunLogEntry[];
50
+ runsTotal: number;
51
+ runsHasMore: boolean;
52
+ runsLoadingMore: boolean;
53
+ runsScope: CronRunScope;
54
+ runsStatuses: CronRunsStatusValue[];
55
+ runsDeliveryStatuses: CronDeliveryStatus[];
56
+ runsStatusFilter: CronRunsStatusFilter;
57
+ runsQuery: string;
58
+ runsSortDir: CronSortDir;
59
+ agentSuggestions: string[];
60
+ modelSuggestions: string[];
61
+ thinkingSuggestions: string[];
62
+ timezoneSuggestions: string[];
63
+ deliveryToSuggestions: string[];
64
+ accountSuggestions: string[];
65
+ onFormChange: (patch: Partial<CronFormState>) => void;
66
+ onRefresh: () => void;
67
+ onAdd: () => void;
68
+ onEdit: (job: CronJob) => void;
69
+ onClone: (job: CronJob) => void;
70
+ onCancelEdit: () => void;
71
+ onToggle: (job: CronJob, enabled: boolean) => void;
72
+ onRun: (job: CronJob, mode?: "force" | "due") => void;
73
+ onRemove: (job: CronJob) => void;
74
+ onLoadRuns: (jobId: string) => void;
75
+ onLoadMoreJobs: () => void;
76
+ onJobsFiltersChange: (patch: {
77
+ cronJobsQuery?: string;
78
+ cronJobsEnabledFilter?: CronJobsEnabledFilter;
79
+ cronJobsScheduleKindFilter?: CronJobsScheduleKindFilter;
80
+ cronJobsLastStatusFilter?: CronJobsLastStatusFilter;
81
+ cronJobsSortBy?: CronJobsSortBy;
82
+ cronJobsSortDir?: CronSortDir;
83
+ }) => void | Promise<void>;
84
+ onJobsFiltersReset: () => void | Promise<void>;
85
+ onLoadMoreRuns: () => void;
86
+ onRunsFiltersChange: (patch: {
87
+ cronRunsScope?: CronRunScope;
88
+ cronRunsStatuses?: CronRunsStatusValue[];
89
+ cronRunsDeliveryStatuses?: CronDeliveryStatus[];
90
+ cronRunsStatusFilter?: CronRunsStatusFilter;
91
+ cronRunsQuery?: string;
92
+ cronRunsSortDir?: CronSortDir;
93
+ }) => void | Promise<void>;
94
+ };
95
+
96
+ function getRunStatusOptions(): Array<{ value: CronRunsStatusValue; label: string }> {
97
+ return [
98
+ { value: "ok", label: t("cron.runs.runStatusOk") },
99
+ { value: "error", label: t("cron.runs.runStatusError") },
100
+ { value: "skipped", label: t("cron.runs.runStatusSkipped") },
101
+ ];
102
+ }
103
+
104
+ function getRunDeliveryOptions(): Array<{ value: CronDeliveryStatus; label: string }> {
105
+ return [
106
+ { value: "delivered", label: t("cron.runs.deliveryDelivered") },
107
+ { value: "not-delivered", label: t("cron.runs.deliveryNotDelivered") },
108
+ { value: "unknown", label: t("cron.runs.deliveryUnknown") },
109
+ { value: "not-requested", label: t("cron.runs.deliveryNotRequested") },
110
+ ];
111
+ }
112
+
113
+ function toggleSelection<T extends string>(selected: T[], value: T, checked: boolean): T[] {
114
+ const set = new Set(selected);
115
+ if (checked) {
116
+ set.add(value);
117
+ } else {
118
+ set.delete(value);
119
+ }
120
+ return Array.from(set);
121
+ }
122
+
123
+ function summarizeSelection(selectedLabels: string[], allLabel: string) {
124
+ if (selectedLabels.length === 0) {
125
+ return allLabel;
126
+ }
127
+ if (selectedLabels.length <= 2) {
128
+ return selectedLabels.join(", ");
129
+ }
130
+ return `${selectedLabels[0]} +${selectedLabels.length - 1}`;
131
+ }
132
+
133
+ function buildChannelOptions(props: CronProps): string[] {
134
+ const options = ["last", ...props.channels.filter(Boolean)];
135
+ const current = props.form.deliveryChannel?.trim();
136
+ if (current && !options.includes(current)) {
137
+ options.push(current);
138
+ }
139
+ const seen = new Set<string>();
140
+ return options.filter((value) => {
141
+ if (seen.has(value)) {
142
+ return false;
143
+ }
144
+ seen.add(value);
145
+ return true;
146
+ });
147
+ }
148
+
149
+ function resolveChannelLabel(props: CronProps, channel: string): string {
150
+ if (channel === "last") {
151
+ return "last";
152
+ }
153
+ const meta = props.channelMeta?.find((entry) => entry.id === channel);
154
+ if (meta?.label) {
155
+ return meta.label;
156
+ }
157
+ return props.channelLabels?.[channel] ?? channel;
158
+ }
159
+
160
+ function renderRunFilterDropdown(params: {
161
+ id: string;
162
+ title: string;
163
+ summary: string;
164
+ options: Array<{ value: string; label: string }>;
165
+ selected: string[];
166
+ onToggle: (value: string, checked: boolean) => void;
167
+ onClear: () => void;
168
+ }) {
169
+ return html`
170
+ <div class="field cron-filter-dropdown" data-filter=${params.id}>
171
+ <span>${params.title}</span>
172
+ <details class="cron-filter-dropdown__details">
173
+ <summary class="btn cron-filter-dropdown__trigger">
174
+ <span>${params.summary}</span>
175
+ </summary>
176
+ <div class="cron-filter-dropdown__panel">
177
+ <div class="cron-filter-dropdown__list">
178
+ ${params.options.map(
179
+ (option) => html`
180
+ <label class="cron-filter-dropdown__option">
181
+ <input
182
+ type="checkbox"
183
+ value=${option.value}
184
+ .checked=${params.selected.includes(option.value)}
185
+ @change=${(event: Event) => {
186
+ const target = event.target as HTMLInputElement;
187
+ params.onToggle(option.value, target.checked);
188
+ }}
189
+ />
190
+ <span>${option.label}</span>
191
+ </label>
192
+ `,
193
+ )}
194
+ </div>
195
+ <div class="row">
196
+ <button class="btn" type="button" @click=${params.onClear}>${t("cron.runs.clear")}</button>
197
+ </div>
198
+ </div>
199
+ </details>
200
+ </div>
201
+ `;
202
+ }
203
+
204
+ function renderSuggestionList(id: string, options: string[]) {
205
+ const clean = Array.from(new Set(options.map((option) => option.trim()).filter(Boolean)));
206
+ if (clean.length === 0) {
207
+ return nothing;
208
+ }
209
+ return html`<datalist id=${id}>
210
+ ${clean.map((value) => html`<option value=${value}></option> `)}
211
+ </datalist>`;
212
+ }
213
+
214
+ type BlockingField = {
215
+ key: CronFieldKey;
216
+ label: string;
217
+ message: string;
218
+ inputId: string;
219
+ };
220
+
221
+ function errorIdForField(key: CronFieldKey) {
222
+ return `cron-error-${key}`;
223
+ }
224
+
225
+ function inputIdForField(key: CronFieldKey) {
226
+ if (key === "name") {
227
+ return "cron-name";
228
+ }
229
+ if (key === "scheduleAt") {
230
+ return "cron-schedule-at";
231
+ }
232
+ if (key === "everyAmount") {
233
+ return "cron-every-amount";
234
+ }
235
+ if (key === "cronExpr") {
236
+ return "cron-cron-expr";
237
+ }
238
+ if (key === "staggerAmount") {
239
+ return "cron-stagger-amount";
240
+ }
241
+ if (key === "payloadText") {
242
+ return "cron-payload-text";
243
+ }
244
+ if (key === "payloadModel") {
245
+ return "cron-payload-model";
246
+ }
247
+ if (key === "payloadThinking") {
248
+ return "cron-payload-thinking";
249
+ }
250
+ if (key === "timeoutSeconds") {
251
+ return "cron-timeout-seconds";
252
+ }
253
+ if (key === "failureAlertAfter") {
254
+ return "cron-failure-alert-after";
255
+ }
256
+ if (key === "failureAlertCooldownSeconds") {
257
+ return "cron-failure-alert-cooldown-seconds";
258
+ }
259
+ return "cron-delivery-to";
260
+ }
261
+
262
+ function fieldLabelForKey(
263
+ key: CronFieldKey,
264
+ form: CronFormState,
265
+ deliveryMode: CronFormState["deliveryMode"],
266
+ ) {
267
+ if (key === "payloadText") {
268
+ return form.payloadKind === "systemEvent"
269
+ ? t("cron.form.mainTimelineMessage")
270
+ : t("cron.form.assistantTaskPrompt");
271
+ }
272
+ if (key === "deliveryTo") {
273
+ return deliveryMode === "webhook" ? t("cron.form.webhookUrl") : t("cron.form.to");
274
+ }
275
+ const labels: Record<CronFieldKey, string> = {
276
+ name: t("cron.form.fieldName"),
277
+ scheduleAt: t("cron.form.runAt"),
278
+ everyAmount: t("cron.form.every"),
279
+ cronExpr: t("cron.form.expression"),
280
+ staggerAmount: t("cron.form.staggerWindow"),
281
+ payloadText: t("cron.form.assistantTaskPrompt"),
282
+ payloadModel: t("cron.form.model"),
283
+ payloadThinking: t("cron.form.thinking"),
284
+ timeoutSeconds: t("cron.form.timeoutSeconds"),
285
+ deliveryTo: t("cron.form.to"),
286
+ failureAlertAfter: "Failure alert after",
287
+ failureAlertCooldownSeconds: "Failure alert cooldown",
288
+ };
289
+ return labels[key];
290
+ }
291
+
292
+ function collectBlockingFields(
293
+ errors: CronFieldErrors,
294
+ form: CronFormState,
295
+ deliveryMode: CronFormState["deliveryMode"],
296
+ ): BlockingField[] {
297
+ const orderedKeys: CronFieldKey[] = [
298
+ "name",
299
+ "scheduleAt",
300
+ "everyAmount",
301
+ "cronExpr",
302
+ "staggerAmount",
303
+ "payloadText",
304
+ "payloadModel",
305
+ "payloadThinking",
306
+ "timeoutSeconds",
307
+ "deliveryTo",
308
+ "failureAlertAfter",
309
+ "failureAlertCooldownSeconds",
310
+ ];
311
+ const fields: BlockingField[] = [];
312
+ for (const key of orderedKeys) {
313
+ const message = errors[key];
314
+ if (!message) {
315
+ continue;
316
+ }
317
+ fields.push({
318
+ key,
319
+ label: fieldLabelForKey(key, form, deliveryMode),
320
+ message,
321
+ inputId: inputIdForField(key),
322
+ });
323
+ }
324
+ return fields;
325
+ }
326
+
327
+ function focusFormField(id: string) {
328
+ const el = document.getElementById(id);
329
+ if (!(el instanceof HTMLElement)) {
330
+ return;
331
+ }
332
+ if (typeof el.scrollIntoView === "function") {
333
+ el.scrollIntoView({ block: "center", behavior: "smooth" });
334
+ }
335
+ el.focus();
336
+ }
337
+
338
+ function renderFieldLabel(text: string, required = false) {
339
+ return html`<span>
340
+ ${text}
341
+ ${
342
+ required
343
+ ? html`
344
+ <span class="cron-required-marker" aria-hidden="true">*</span>
345
+ <span class="cron-required-sr">${t("cron.form.requiredSr")}</span>
346
+ `
347
+ : nothing
348
+ }
349
+ </span>`;
350
+ }
351
+
352
+ export function renderCron(props: CronProps) {
353
+ const isEditing = Boolean(props.editingJobId);
354
+ const isAgentTurn = props.form.payloadKind === "agentTurn";
355
+ const isCronSchedule = props.form.scheduleKind === "cron";
356
+ const channelOptions = buildChannelOptions(props);
357
+ const selectedJob =
358
+ props.runsJobId == null ? undefined : props.jobs.find((job) => job.id === props.runsJobId);
359
+ const selectedRunTitle =
360
+ props.runsScope === "all"
361
+ ? t("cron.jobList.allJobs")
362
+ : (selectedJob?.name ?? props.runsJobId ?? t("cron.jobList.selectJob"));
363
+ const runs = props.runs;
364
+ const runStatusOptions = getRunStatusOptions();
365
+ const runDeliveryOptions = getRunDeliveryOptions();
366
+ const selectedStatusLabels = runStatusOptions
367
+ .filter((option) => props.runsStatuses.includes(option.value))
368
+ .map((option) => option.label);
369
+ const selectedDeliveryLabels = runDeliveryOptions
370
+ .filter((option) => props.runsDeliveryStatuses.includes(option.value))
371
+ .map((option) => option.label);
372
+ const statusSummary = summarizeSelection(selectedStatusLabels, t("cron.runs.allStatuses"));
373
+ const deliverySummary = summarizeSelection(selectedDeliveryLabels, t("cron.runs.allDelivery"));
374
+ const supportsAnnounce =
375
+ props.form.sessionTarget === "isolated" && props.form.payloadKind === "agentTurn";
376
+ const selectedDeliveryMode =
377
+ props.form.deliveryMode === "announce" && !supportsAnnounce ? "none" : props.form.deliveryMode;
378
+ const blockingFields = collectBlockingFields(props.fieldErrors, props.form, selectedDeliveryMode);
379
+ const blockedByValidation = !props.busy && blockingFields.length > 0;
380
+ const hasActiveJobsFilters =
381
+ props.jobsQuery.trim().length > 0 ||
382
+ props.jobsEnabledFilter !== "all" ||
383
+ props.jobsScheduleKindFilter !== "all" ||
384
+ props.jobsLastStatusFilter !== "all" ||
385
+ props.jobsSortBy !== "nextRunAtMs" ||
386
+ props.jobsSortDir !== "asc";
387
+ const submitDisabledReason =
388
+ blockedByValidation && !props.canSubmit
389
+ ? blockingFields.length === 1
390
+ ? t("cron.form.fixFields", { count: String(blockingFields.length) })
391
+ : t("cron.form.fixFieldsPlural", { count: String(blockingFields.length) })
392
+ : "";
393
+ return html`
394
+ <section class="card cron-summary-strip">
395
+ <div class="cron-summary-strip__left">
396
+ <div class="cron-summary-item">
397
+ <div class="cron-summary-label">${t("cron.summary.enabled")}</div>
398
+ <div class="cron-summary-value">
399
+ <span class=${`chip ${props.status?.enabled ? "chip-ok" : "chip-danger"}`}>
400
+ ${
401
+ props.status
402
+ ? props.status.enabled
403
+ ? t("cron.summary.yes")
404
+ : t("cron.summary.no")
405
+ : t("common.na")
406
+ }
407
+ </span>
408
+ </div>
409
+ </div>
410
+ <div class="cron-summary-item">
411
+ <div class="cron-summary-label">${t("cron.summary.jobs")}</div>
412
+ <div class="cron-summary-value">${props.status?.jobs ?? t("common.na")}</div>
413
+ </div>
414
+ <div class="cron-summary-item cron-summary-item--wide">
415
+ <div class="cron-summary-label">${t("cron.summary.nextWake")}</div>
416
+ <div class="cron-summary-value">${formatNextRun(props.status?.nextWakeAtMs ?? null)}</div>
417
+ </div>
418
+ </div>
419
+ <div class="cron-summary-strip__actions">
420
+ <button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
421
+ ${props.loading ? t("cron.summary.refreshing") : t("cron.summary.refresh")}
422
+ </button>
423
+ ${props.error ? html`<span class="muted">${props.error}</span>` : nothing}
424
+ </div>
425
+ </section>
426
+
427
+ <section class="cron-workspace">
428
+ <div class="cron-workspace-main">
429
+ <section class="card">
430
+ <div class="row" style="justify-content: space-between; align-items: flex-start; gap: 12px;">
431
+ <div>
432
+ <div class="card-title">${t("cron.jobs.title")}</div>
433
+ <div class="card-sub">${t("cron.jobs.subtitle")}</div>
434
+ </div>
435
+ <div class="muted">${t("cron.jobs.shownOf", {
436
+ shown: String(props.jobs.length),
437
+ total: String(props.jobsTotal),
438
+ })}</div>
439
+ </div>
440
+ <div class="filters" style="margin-top: 12px;">
441
+ <label class="field cron-filter-search">
442
+ <span>${t("cron.jobs.searchJobs")}</span>
443
+ <input
444
+ .value=${props.jobsQuery}
445
+ placeholder=${t("cron.jobs.searchPlaceholder")}
446
+ @input=${(e: Event) =>
447
+ props.onJobsFiltersChange({
448
+ cronJobsQuery: (e.target as HTMLInputElement).value,
449
+ })}
450
+ />
451
+ </label>
452
+ <label class="field">
453
+ <span>${t("cron.jobs.enabled")}</span>
454
+ <select
455
+ .value=${props.jobsEnabledFilter}
456
+ @change=${(e: Event) =>
457
+ props.onJobsFiltersChange({
458
+ cronJobsEnabledFilter: (e.target as HTMLSelectElement)
459
+ .value as CronJobsEnabledFilter,
460
+ })}
461
+ >
462
+ <option value="all">${t("cron.jobs.all")}</option>
463
+ <option value="enabled">${t("common.enabled")}</option>
464
+ <option value="disabled">${t("common.disabled")}</option>
465
+ </select>
466
+ </label>
467
+ <label class="field">
468
+ <span>${t("cron.jobs.schedule")}</span>
469
+ <select
470
+ data-test-id="cron-jobs-schedule-filter"
471
+ .value=${props.jobsScheduleKindFilter}
472
+ @change=${(e: Event) =>
473
+ props.onJobsFiltersChange({
474
+ cronJobsScheduleKindFilter: (e.target as HTMLSelectElement)
475
+ .value as CronJobsScheduleKindFilter,
476
+ })}
477
+ >
478
+ <option value="all">${t("cron.jobs.all")}</option>
479
+ <option value="at">${t("cron.form.at")}</option>
480
+ <option value="every">${t("cron.form.every")}</option>
481
+ <option value="cron">${t("cron.form.cronOption")}</option>
482
+ </select>
483
+ </label>
484
+ <label class="field">
485
+ <span>${t("cron.jobs.lastRun")}</span>
486
+ <select
487
+ data-test-id="cron-jobs-last-status-filter"
488
+ .value=${props.jobsLastStatusFilter}
489
+ @change=${(e: Event) =>
490
+ props.onJobsFiltersChange({
491
+ cronJobsLastStatusFilter: (e.target as HTMLSelectElement)
492
+ .value as CronJobsLastStatusFilter,
493
+ })}
494
+ >
495
+ <option value="all">${t("cron.jobs.all")}</option>
496
+ <option value="ok">${t("cron.runs.runStatusOk")}</option>
497
+ <option value="error">${t("cron.runs.runStatusError")}</option>
498
+ <option value="skipped">${t("cron.runs.runStatusSkipped")}</option>
499
+ </select>
500
+ </label>
501
+ <label class="field">
502
+ <span>${t("cron.jobs.sort")}</span>
503
+ <select
504
+ .value=${props.jobsSortBy}
505
+ @change=${(e: Event) =>
506
+ props.onJobsFiltersChange({
507
+ cronJobsSortBy: (e.target as HTMLSelectElement).value as CronJobsSortBy,
508
+ })}
509
+ >
510
+ <option value="nextRunAtMs">${t("cron.jobs.nextRun")}</option>
511
+ <option value="updatedAtMs">${t("cron.jobs.recentlyUpdated")}</option>
512
+ <option value="name">${t("cron.jobs.name")}</option>
513
+ </select>
514
+ </label>
515
+ <label class="field">
516
+ <span>${t("cron.jobs.direction")}</span>
517
+ <select
518
+ .value=${props.jobsSortDir}
519
+ @change=${(e: Event) =>
520
+ props.onJobsFiltersChange({
521
+ cronJobsSortDir: (e.target as HTMLSelectElement).value as CronSortDir,
522
+ })}
523
+ >
524
+ <option value="asc">${t("cron.jobs.ascending")}</option>
525
+ <option value="desc">${t("cron.jobs.descending")}</option>
526
+ </select>
527
+ </label>
528
+ <label class="field">
529
+ <span>${t("cron.jobs.reset")}</span>
530
+ <button
531
+ class="btn"
532
+ data-test-id="cron-jobs-filters-reset"
533
+ ?disabled=${!hasActiveJobsFilters}
534
+ @click=${props.onJobsFiltersReset}
535
+ >
536
+ ${t("cron.jobs.reset")}
537
+ </button>
538
+ </label>
539
+ </div>
540
+ ${
541
+ props.jobs.length === 0
542
+ ? html`
543
+ <div class="muted" style="margin-top: 12px">${t("cron.jobs.noMatching")}</div>
544
+ `
545
+ : html`
546
+ <div class="list" style="margin-top: 12px;">
547
+ ${props.jobs.map((job) => renderJob(job, props))}
548
+ </div>
549
+ `
550
+ }
551
+ ${
552
+ props.jobsHasMore
553
+ ? html`
554
+ <div class="row" style="margin-top: 12px">
555
+ <button
556
+ class="btn"
557
+ ?disabled=${props.loading || props.jobsLoadingMore}
558
+ @click=${props.onLoadMoreJobs}
559
+ >
560
+ ${props.jobsLoadingMore ? t("cron.jobs.loading") : t("cron.jobs.loadMore")}
561
+ </button>
562
+ </div>
563
+ `
564
+ : nothing
565
+ }
566
+ </section>
567
+
568
+ <section class="card">
569
+ <div class="row" style="justify-content: space-between; align-items: flex-start; gap: 12px;">
570
+ <div>
571
+ <div class="card-title">${t("cron.runs.title")}</div>
572
+ <div class="card-sub">
573
+ ${
574
+ props.runsScope === "all"
575
+ ? t("cron.runs.subtitleAll")
576
+ : t("cron.runs.subtitleJob", { title: selectedRunTitle })
577
+ }
578
+ </div>
579
+ </div>
580
+ <div class="muted">${t("cron.jobs.shownOf", {
581
+ shown: String(runs.length),
582
+ total: String(props.runsTotal),
583
+ })}</div>
584
+ </div>
585
+ <div class="cron-run-filters">
586
+ <div class="cron-run-filters__row cron-run-filters__row--primary">
587
+ <label class="field">
588
+ <span>${t("cron.runs.scope")}</span>
589
+ <select
590
+ .value=${props.runsScope}
591
+ @change=${(e: Event) =>
592
+ props.onRunsFiltersChange({
593
+ cronRunsScope: (e.target as HTMLSelectElement).value as CronRunScope,
594
+ })}
595
+ >
596
+ <option value="all">${t("cron.runs.allJobs")}</option>
597
+ <option value="job" ?disabled=${props.runsJobId == null}>${t("cron.runs.selectedJob")}</option>
598
+ </select>
599
+ </label>
600
+ <label class="field cron-run-filter-search">
601
+ <span>${t("cron.runs.searchRuns")}</span>
602
+ <input
603
+ .value=${props.runsQuery}
604
+ placeholder=${t("cron.runs.searchPlaceholder")}
605
+ @input=${(e: Event) =>
606
+ props.onRunsFiltersChange({
607
+ cronRunsQuery: (e.target as HTMLInputElement).value,
608
+ })}
609
+ />
610
+ </label>
611
+ <label class="field">
612
+ <span>${t("cron.jobs.sort")}</span>
613
+ <select
614
+ .value=${props.runsSortDir}
615
+ @change=${(e: Event) =>
616
+ props.onRunsFiltersChange({
617
+ cronRunsSortDir: (e.target as HTMLSelectElement).value as CronSortDir,
618
+ })}
619
+ >
620
+ <option value="desc">${t("cron.runs.newestFirst")}</option>
621
+ <option value="asc">${t("cron.runs.oldestFirst")}</option>
622
+ </select>
623
+ </label>
624
+ </div>
625
+ <div class="cron-run-filters__row cron-run-filters__row--secondary">
626
+ ${renderRunFilterDropdown({
627
+ id: "status",
628
+ title: t("cron.runs.status"),
629
+ summary: statusSummary,
630
+ options: runStatusOptions,
631
+ selected: props.runsStatuses,
632
+ onToggle: (value, checked) => {
633
+ const next = toggleSelection(
634
+ props.runsStatuses,
635
+ value as CronRunsStatusValue,
636
+ checked,
637
+ );
638
+ void props.onRunsFiltersChange({ cronRunsStatuses: next });
639
+ },
640
+ onClear: () => {
641
+ void props.onRunsFiltersChange({ cronRunsStatuses: [] });
642
+ },
643
+ })}
644
+ ${renderRunFilterDropdown({
645
+ id: "delivery",
646
+ title: t("cron.runs.delivery"),
647
+ summary: deliverySummary,
648
+ options: runDeliveryOptions,
649
+ selected: props.runsDeliveryStatuses,
650
+ onToggle: (value, checked) => {
651
+ const next = toggleSelection(
652
+ props.runsDeliveryStatuses,
653
+ value as CronDeliveryStatus,
654
+ checked,
655
+ );
656
+ void props.onRunsFiltersChange({ cronRunsDeliveryStatuses: next });
657
+ },
658
+ onClear: () => {
659
+ void props.onRunsFiltersChange({ cronRunsDeliveryStatuses: [] });
660
+ },
661
+ })}
662
+ </div>
663
+ </div>
664
+ ${
665
+ props.runsScope === "job" && props.runsJobId == null
666
+ ? html`
667
+ <div class="muted" style="margin-top: 12px">${t("cron.runs.selectJobHint")}</div>
668
+ `
669
+ : runs.length === 0
670
+ ? html`
671
+ <div class="muted" style="margin-top: 12px">${t("cron.runs.noMatching")}</div>
672
+ `
673
+ : html`
674
+ <div class="list" style="margin-top: 12px;">
675
+ ${runs.map((entry) => renderRun(entry, props.basePath))}
676
+ </div>
677
+ `
678
+ }
679
+ ${
680
+ (props.runsScope === "all" || props.runsJobId != null) && props.runsHasMore
681
+ ? html`
682
+ <div class="row" style="margin-top: 12px">
683
+ <button
684
+ class="btn"
685
+ ?disabled=${props.runsLoadingMore}
686
+ @click=${props.onLoadMoreRuns}
687
+ >
688
+ ${props.runsLoadingMore ? t("cron.jobs.loading") : t("cron.runs.loadMore")}
689
+ </button>
690
+ </div>
691
+ `
692
+ : nothing
693
+ }
694
+ </section>
695
+ </div>
696
+
697
+ <section class="card cron-workspace-form">
698
+ <div class="card-title">${isEditing ? t("cron.form.editJob") : t("cron.form.newJob")}</div>
699
+ <div class="card-sub">
700
+ ${isEditing ? t("cron.form.updateSubtitle") : t("cron.form.createSubtitle")}
701
+ </div>
702
+ <div class="cron-form">
703
+ <div class="cron-required-legend">
704
+ <span class="cron-required-marker" aria-hidden="true">*</span> ${t("cron.form.required")}
705
+ </div>
706
+ <section class="cron-form-section">
707
+ <div class="cron-form-section__title">${t("cron.form.basics")}</div>
708
+ <div class="cron-form-section__sub">${t("cron.form.basicsSub")}</div>
709
+ <div class="form-grid cron-form-grid">
710
+ <label class="field">
711
+ ${renderFieldLabel(t("cron.form.fieldName"), true)}
712
+ <input
713
+ id="cron-name"
714
+ .value=${props.form.name}
715
+ placeholder=${t("cron.form.namePlaceholder")}
716
+ aria-invalid=${props.fieldErrors.name ? "true" : "false"}
717
+ aria-describedby=${ifDefined(
718
+ props.fieldErrors.name ? errorIdForField("name") : undefined,
719
+ )}
720
+ @input=${(e: Event) =>
721
+ props.onFormChange({ name: (e.target as HTMLInputElement).value })}
722
+ />
723
+ ${renderFieldError(props.fieldErrors.name, errorIdForField("name"))}
724
+ </label>
725
+ <label class="field">
726
+ <span>${t("cron.form.description")}</span>
727
+ <input
728
+ .value=${props.form.description}
729
+ placeholder=${t("cron.form.descriptionPlaceholder")}
730
+ @input=${(e: Event) =>
731
+ props.onFormChange({ description: (e.target as HTMLInputElement).value })}
732
+ />
733
+ </label>
734
+ <label class="field">
735
+ ${renderFieldLabel(t("cron.form.agentId"))}
736
+ <input
737
+ id="cron-agent-id"
738
+ .value=${props.form.agentId}
739
+ list="cron-agent-suggestions"
740
+ ?disabled=${props.form.clearAgent}
741
+ @input=${(e: Event) =>
742
+ props.onFormChange({ agentId: (e.target as HTMLInputElement).value })}
743
+ placeholder=${t("cron.form.agentPlaceholder")}
744
+ />
745
+ <div class="cron-help">${t("cron.form.agentHelp")}</div>
746
+ </label>
747
+ <label class="field checkbox cron-checkbox cron-checkbox-inline">
748
+ <input
749
+ type="checkbox"
750
+ .checked=${props.form.enabled}
751
+ @change=${(e: Event) =>
752
+ props.onFormChange({ enabled: (e.target as HTMLInputElement).checked })}
753
+ />
754
+ <span class="field-checkbox__label">${t("cron.summary.enabled")}</span>
755
+ </label>
756
+ </div>
757
+ </section>
758
+
759
+ <section class="cron-form-section">
760
+ <div class="cron-form-section__title">${t("cron.form.schedule")}</div>
761
+ <div class="cron-form-section__sub">${t("cron.form.scheduleSub")}</div>
762
+ <div class="form-grid cron-form-grid">
763
+ <label class="field cron-span-2">
764
+ ${renderFieldLabel(t("cron.form.schedule"))}
765
+ <select
766
+ id="cron-schedule-kind"
767
+ .value=${props.form.scheduleKind}
768
+ @change=${(e: Event) =>
769
+ props.onFormChange({
770
+ scheduleKind: (e.target as HTMLSelectElement)
771
+ .value as CronFormState["scheduleKind"],
772
+ })}
773
+ >
774
+ <option value="every">${t("cron.form.every")}</option>
775
+ <option value="at">${t("cron.form.at")}</option>
776
+ <option value="cron">${t("cron.form.cronOption")}</option>
777
+ </select>
778
+ </label>
779
+ </div>
780
+ ${renderScheduleFields(props)}
781
+ </section>
782
+
783
+ <section class="cron-form-section">
784
+ <div class="cron-form-section__title">${t("cron.form.execution")}</div>
785
+ <div class="cron-form-section__sub">${t("cron.form.executionSub")}</div>
786
+ <div class="form-grid cron-form-grid">
787
+ <label class="field">
788
+ ${renderFieldLabel(t("cron.form.session"))}
789
+ <select
790
+ id="cron-session-target"
791
+ .value=${props.form.sessionTarget}
792
+ @change=${(e: Event) =>
793
+ props.onFormChange({
794
+ sessionTarget: (e.target as HTMLSelectElement)
795
+ .value as CronFormState["sessionTarget"],
796
+ })}
797
+ >
798
+ <option value="main">${t("cron.form.main")}</option>
799
+ <option value="isolated">${t("cron.form.isolated")}</option>
800
+ </select>
801
+ <div class="cron-help">${t("cron.form.sessionHelp")}</div>
802
+ </label>
803
+ <label class="field">
804
+ ${renderFieldLabel(t("cron.form.wakeMode"))}
805
+ <select
806
+ id="cron-wake-mode"
807
+ .value=${props.form.wakeMode}
808
+ @change=${(e: Event) =>
809
+ props.onFormChange({
810
+ wakeMode: (e.target as HTMLSelectElement).value as CronFormState["wakeMode"],
811
+ })}
812
+ >
813
+ <option value="now">${t("cron.form.now")}</option>
814
+ <option value="next-heartbeat">${t("cron.form.nextHeartbeat")}</option>
815
+ </select>
816
+ <div class="cron-help">${t("cron.form.wakeModeHelp")}</div>
817
+ </label>
818
+ <label class="field ${isAgentTurn ? "" : "cron-span-2"}">
819
+ ${renderFieldLabel(t("cron.form.payloadKind"))}
820
+ <select
821
+ id="cron-payload-kind"
822
+ .value=${props.form.payloadKind}
823
+ @change=${(e: Event) =>
824
+ props.onFormChange({
825
+ payloadKind: (e.target as HTMLSelectElement)
826
+ .value as CronFormState["payloadKind"],
827
+ })}
828
+ >
829
+ <option value="systemEvent">${t("cron.form.systemEvent")}</option>
830
+ <option value="agentTurn">${t("cron.form.agentTurn")}</option>
831
+ </select>
832
+ <div class="cron-help">
833
+ ${
834
+ props.form.payloadKind === "systemEvent"
835
+ ? t("cron.form.systemEventHelp")
836
+ : t("cron.form.agentTurnHelp")
837
+ }
838
+ </div>
839
+ </label>
840
+ ${
841
+ isAgentTurn
842
+ ? html`
843
+ <label class="field">
844
+ ${renderFieldLabel(t("cron.form.timeoutSeconds"))}
845
+ <input
846
+ id="cron-timeout-seconds"
847
+ .value=${props.form.timeoutSeconds}
848
+ placeholder=${t("cron.form.timeoutPlaceholder")}
849
+ aria-invalid=${props.fieldErrors.timeoutSeconds ? "true" : "false"}
850
+ aria-describedby=${ifDefined(
851
+ props.fieldErrors.timeoutSeconds
852
+ ? errorIdForField("timeoutSeconds")
853
+ : undefined,
854
+ )}
855
+ @input=${(e: Event) =>
856
+ props.onFormChange({
857
+ timeoutSeconds: (e.target as HTMLInputElement).value,
858
+ })}
859
+ />
860
+ <div class="cron-help">${t("cron.form.timeoutHelp")}</div>
861
+ ${renderFieldError(
862
+ props.fieldErrors.timeoutSeconds,
863
+ errorIdForField("timeoutSeconds"),
864
+ )}
865
+ </label>
866
+ `
867
+ : nothing
868
+ }
869
+ </div>
870
+ <label class="field cron-span-2">
871
+ ${renderFieldLabel(
872
+ props.form.payloadKind === "systemEvent"
873
+ ? t("cron.form.mainTimelineMessage")
874
+ : t("cron.form.assistantTaskPrompt"),
875
+ true,
876
+ )}
877
+ <textarea
878
+ id="cron-payload-text"
879
+ .value=${props.form.payloadText}
880
+ aria-invalid=${props.fieldErrors.payloadText ? "true" : "false"}
881
+ aria-describedby=${ifDefined(
882
+ props.fieldErrors.payloadText ? errorIdForField("payloadText") : undefined,
883
+ )}
884
+ @input=${(e: Event) =>
885
+ props.onFormChange({
886
+ payloadText: (e.target as HTMLTextAreaElement).value,
887
+ })}
888
+ rows="4"
889
+ ></textarea>
890
+ ${renderFieldError(props.fieldErrors.payloadText, errorIdForField("payloadText"))}
891
+ </label>
892
+ </section>
893
+
894
+ <section class="cron-form-section">
895
+ <div class="cron-form-section__title">${t("cron.form.deliverySection")}</div>
896
+ <div class="cron-form-section__sub">${t("cron.form.deliverySub")}</div>
897
+ <div class="form-grid cron-form-grid">
898
+ <label class="field ${selectedDeliveryMode === "none" ? "cron-span-2" : ""}">
899
+ ${renderFieldLabel(t("cron.form.resultDelivery"))}
900
+ <select
901
+ id="cron-delivery-mode"
902
+ .value=${selectedDeliveryMode}
903
+ @change=${(e: Event) =>
904
+ props.onFormChange({
905
+ deliveryMode: (e.target as HTMLSelectElement)
906
+ .value as CronFormState["deliveryMode"],
907
+ })}
908
+ >
909
+ ${
910
+ supportsAnnounce
911
+ ? html`
912
+ <option value="announce">${t("cron.form.announceDefault")}</option>
913
+ `
914
+ : nothing
915
+ }
916
+ <option value="webhook">${t("cron.form.webhookPost")}</option>
917
+ <option value="none">${t("cron.form.noneInternal")}</option>
918
+ </select>
919
+ <div class="cron-help">${t("cron.form.deliveryHelp")}</div>
920
+ </label>
921
+ ${
922
+ selectedDeliveryMode !== "none"
923
+ ? html`
924
+ <label class="field ${selectedDeliveryMode === "webhook" ? "cron-span-2" : ""}">
925
+ ${renderFieldLabel(
926
+ selectedDeliveryMode === "webhook"
927
+ ? t("cron.form.webhookUrl")
928
+ : t("cron.form.channel"),
929
+ selectedDeliveryMode === "webhook",
930
+ )}
931
+ ${
932
+ selectedDeliveryMode === "webhook"
933
+ ? html`
934
+ <input
935
+ id="cron-delivery-to"
936
+ .value=${props.form.deliveryTo}
937
+ list="cron-delivery-to-suggestions"
938
+ aria-invalid=${props.fieldErrors.deliveryTo ? "true" : "false"}
939
+ aria-describedby=${ifDefined(
940
+ props.fieldErrors.deliveryTo
941
+ ? errorIdForField("deliveryTo")
942
+ : undefined,
943
+ )}
944
+ @input=${(e: Event) =>
945
+ props.onFormChange({
946
+ deliveryTo: (e.target as HTMLInputElement).value,
947
+ })}
948
+ placeholder=${t("cron.form.webhookPlaceholder")}
949
+ />
950
+ `
951
+ : html`
952
+ <select
953
+ id="cron-delivery-channel"
954
+ .value=${props.form.deliveryChannel || "last"}
955
+ @change=${(e: Event) =>
956
+ props.onFormChange({
957
+ deliveryChannel: (e.target as HTMLSelectElement).value,
958
+ })}
959
+ >
960
+ ${channelOptions.map(
961
+ (channel) =>
962
+ html`<option value=${channel}>
963
+ ${resolveChannelLabel(props, channel)}
964
+ </option>`,
965
+ )}
966
+ </select>
967
+ `
968
+ }
969
+ ${
970
+ selectedDeliveryMode === "announce"
971
+ ? html`
972
+ <div class="cron-help">${t("cron.form.channelHelp")}</div>
973
+ `
974
+ : html`
975
+ <div class="cron-help">${t("cron.form.webhookHelp")}</div>
976
+ `
977
+ }
978
+ </label>
979
+ ${
980
+ selectedDeliveryMode === "announce"
981
+ ? html`
982
+ <label class="field cron-span-2">
983
+ ${renderFieldLabel(t("cron.form.to"))}
984
+ <input
985
+ id="cron-delivery-to"
986
+ .value=${props.form.deliveryTo}
987
+ list="cron-delivery-to-suggestions"
988
+ @input=${(e: Event) =>
989
+ props.onFormChange({
990
+ deliveryTo: (e.target as HTMLInputElement).value,
991
+ })}
992
+ placeholder=${t("cron.form.toPlaceholder")}
993
+ />
994
+ <div class="cron-help">${t("cron.form.toHelp")}</div>
995
+ </label>
996
+ `
997
+ : nothing
998
+ }
999
+ ${
1000
+ selectedDeliveryMode === "webhook"
1001
+ ? renderFieldError(
1002
+ props.fieldErrors.deliveryTo,
1003
+ errorIdForField("deliveryTo"),
1004
+ )
1005
+ : nothing
1006
+ }
1007
+ `
1008
+ : nothing
1009
+ }
1010
+ </div>
1011
+ </section>
1012
+
1013
+ <details class="cron-advanced">
1014
+ <summary class="cron-advanced__summary">${t("cron.form.advanced")}</summary>
1015
+ <div class="cron-help">${t("cron.form.advancedHelp")}</div>
1016
+ <div class="form-grid cron-form-grid">
1017
+ <label class="field checkbox cron-checkbox">
1018
+ <input
1019
+ type="checkbox"
1020
+ .checked=${props.form.deleteAfterRun}
1021
+ @change=${(e: Event) =>
1022
+ props.onFormChange({
1023
+ deleteAfterRun: (e.target as HTMLInputElement).checked,
1024
+ })}
1025
+ />
1026
+ <span class="field-checkbox__label">${t("cron.form.deleteAfterRun")}</span>
1027
+ <div class="cron-help">${t("cron.form.deleteAfterRunHelp")}</div>
1028
+ </label>
1029
+ <label class="field checkbox cron-checkbox">
1030
+ <input
1031
+ type="checkbox"
1032
+ .checked=${props.form.clearAgent}
1033
+ @change=${(e: Event) =>
1034
+ props.onFormChange({
1035
+ clearAgent: (e.target as HTMLInputElement).checked,
1036
+ })}
1037
+ />
1038
+ <span class="field-checkbox__label">${t("cron.form.clearAgentOverride")}</span>
1039
+ <div class="cron-help">${t("cron.form.clearAgentHelp")}</div>
1040
+ </label>
1041
+ <label class="field cron-span-2">
1042
+ ${renderFieldLabel("Session key")}
1043
+ <input
1044
+ id="cron-session-key"
1045
+ .value=${props.form.sessionKey}
1046
+ @input=${(e: Event) =>
1047
+ props.onFormChange({
1048
+ sessionKey: (e.target as HTMLInputElement).value,
1049
+ })}
1050
+ placeholder="agent:main:main"
1051
+ />
1052
+ <div class="cron-help">
1053
+ Optional routing key for job delivery and wake routing.
1054
+ </div>
1055
+ </label>
1056
+ ${
1057
+ isCronSchedule
1058
+ ? html`
1059
+ <label class="field checkbox cron-checkbox cron-span-2">
1060
+ <input
1061
+ type="checkbox"
1062
+ .checked=${props.form.scheduleExact}
1063
+ @change=${(e: Event) =>
1064
+ props.onFormChange({
1065
+ scheduleExact: (e.target as HTMLInputElement).checked,
1066
+ })}
1067
+ />
1068
+ <span class="field-checkbox__label">${t("cron.form.exactTiming")}</span>
1069
+ <div class="cron-help">${t("cron.form.exactTimingHelp")}</div>
1070
+ </label>
1071
+ <div class="cron-stagger-group cron-span-2">
1072
+ <label class="field">
1073
+ ${renderFieldLabel(t("cron.form.staggerWindow"))}
1074
+ <input
1075
+ id="cron-stagger-amount"
1076
+ .value=${props.form.staggerAmount}
1077
+ ?disabled=${props.form.scheduleExact}
1078
+ aria-invalid=${props.fieldErrors.staggerAmount ? "true" : "false"}
1079
+ aria-describedby=${ifDefined(
1080
+ props.fieldErrors.staggerAmount
1081
+ ? errorIdForField("staggerAmount")
1082
+ : undefined,
1083
+ )}
1084
+ @input=${(e: Event) =>
1085
+ props.onFormChange({
1086
+ staggerAmount: (e.target as HTMLInputElement).value,
1087
+ })}
1088
+ placeholder=${t("cron.form.staggerPlaceholder")}
1089
+ />
1090
+ ${renderFieldError(
1091
+ props.fieldErrors.staggerAmount,
1092
+ errorIdForField("staggerAmount"),
1093
+ )}
1094
+ </label>
1095
+ <label class="field">
1096
+ <span>${t("cron.form.staggerUnit")}</span>
1097
+ <select
1098
+ .value=${props.form.staggerUnit}
1099
+ ?disabled=${props.form.scheduleExact}
1100
+ @change=${(e: Event) =>
1101
+ props.onFormChange({
1102
+ staggerUnit: (e.target as HTMLSelectElement)
1103
+ .value as CronFormState["staggerUnit"],
1104
+ })}
1105
+ >
1106
+ <option value="seconds">${t("cron.form.seconds")}</option>
1107
+ <option value="minutes">${t("cron.form.minutes")}</option>
1108
+ </select>
1109
+ </label>
1110
+ </div>
1111
+ `
1112
+ : nothing
1113
+ }
1114
+ ${
1115
+ isAgentTurn
1116
+ ? html`
1117
+ <label class="field cron-span-2">
1118
+ ${renderFieldLabel("Account ID")}
1119
+ <input
1120
+ id="cron-delivery-account-id"
1121
+ .value=${props.form.deliveryAccountId}
1122
+ list="cron-delivery-account-suggestions"
1123
+ ?disabled=${selectedDeliveryMode !== "announce"}
1124
+ @input=${(e: Event) =>
1125
+ props.onFormChange({
1126
+ deliveryAccountId: (e.target as HTMLInputElement).value,
1127
+ })}
1128
+ placeholder="default"
1129
+ />
1130
+ <div class="cron-help">
1131
+ Optional channel account ID for multi-account setups.
1132
+ </div>
1133
+ </label>
1134
+ <label class="field checkbox cron-checkbox cron-span-2">
1135
+ <input
1136
+ type="checkbox"
1137
+ .checked=${props.form.payloadLightContext}
1138
+ @change=${(e: Event) =>
1139
+ props.onFormChange({
1140
+ payloadLightContext: (e.target as HTMLInputElement).checked,
1141
+ })}
1142
+ />
1143
+ <span class="field-checkbox__label">Light context</span>
1144
+ <div class="cron-help">
1145
+ Use lightweight bootstrap context for this agent job.
1146
+ </div>
1147
+ </label>
1148
+ <label class="field">
1149
+ ${renderFieldLabel(t("cron.form.model"))}
1150
+ <input
1151
+ id="cron-payload-model"
1152
+ .value=${props.form.payloadModel}
1153
+ list="cron-model-suggestions"
1154
+ @input=${(e: Event) =>
1155
+ props.onFormChange({
1156
+ payloadModel: (e.target as HTMLInputElement).value,
1157
+ })}
1158
+ placeholder=${t("cron.form.modelPlaceholder")}
1159
+ />
1160
+ <div class="cron-help">${t("cron.form.modelHelp")}</div>
1161
+ </label>
1162
+ <label class="field">
1163
+ ${renderFieldLabel(t("cron.form.thinking"))}
1164
+ <input
1165
+ id="cron-payload-thinking"
1166
+ .value=${props.form.payloadThinking}
1167
+ list="cron-thinking-suggestions"
1168
+ @input=${(e: Event) =>
1169
+ props.onFormChange({
1170
+ payloadThinking: (e.target as HTMLInputElement).value,
1171
+ })}
1172
+ placeholder=${t("cron.form.thinkingPlaceholder")}
1173
+ />
1174
+ <div class="cron-help">${t("cron.form.thinkingHelp")}</div>
1175
+ </label>
1176
+ `
1177
+ : nothing
1178
+ }
1179
+ ${
1180
+ isAgentTurn
1181
+ ? html`
1182
+ <label class="field cron-span-2">
1183
+ ${renderFieldLabel("Failure alerts")}
1184
+ <select
1185
+ .value=${props.form.failureAlertMode}
1186
+ @change=${(e: Event) =>
1187
+ props.onFormChange({
1188
+ failureAlertMode: (e.target as HTMLSelectElement)
1189
+ .value as CronFormState["failureAlertMode"],
1190
+ })}
1191
+ >
1192
+ <option value="inherit">Inherit global setting</option>
1193
+ <option value="disabled">Disable for this job</option>
1194
+ <option value="custom">Custom per-job settings</option>
1195
+ </select>
1196
+ <div class="cron-help">
1197
+ Control when this job sends repeated-failure alerts.
1198
+ </div>
1199
+ </label>
1200
+ ${
1201
+ props.form.failureAlertMode === "custom"
1202
+ ? html`
1203
+ <label class="field">
1204
+ ${renderFieldLabel("Alert after")}
1205
+ <input
1206
+ id="cron-failure-alert-after"
1207
+ .value=${props.form.failureAlertAfter}
1208
+ aria-invalid=${props.fieldErrors.failureAlertAfter ? "true" : "false"}
1209
+ aria-describedby=${ifDefined(
1210
+ props.fieldErrors.failureAlertAfter
1211
+ ? errorIdForField("failureAlertAfter")
1212
+ : undefined,
1213
+ )}
1214
+ @input=${(e: Event) =>
1215
+ props.onFormChange({
1216
+ failureAlertAfter: (e.target as HTMLInputElement).value,
1217
+ })}
1218
+ placeholder="2"
1219
+ />
1220
+ <div class="cron-help">Consecutive errors before alerting.</div>
1221
+ ${renderFieldError(
1222
+ props.fieldErrors.failureAlertAfter,
1223
+ errorIdForField("failureAlertAfter"),
1224
+ )}
1225
+ </label>
1226
+ <label class="field">
1227
+ ${renderFieldLabel("Cooldown (seconds)")}
1228
+ <input
1229
+ id="cron-failure-alert-cooldown-seconds"
1230
+ .value=${props.form.failureAlertCooldownSeconds}
1231
+ aria-invalid=${props.fieldErrors.failureAlertCooldownSeconds ? "true" : "false"}
1232
+ aria-describedby=${ifDefined(
1233
+ props.fieldErrors.failureAlertCooldownSeconds
1234
+ ? errorIdForField("failureAlertCooldownSeconds")
1235
+ : undefined,
1236
+ )}
1237
+ @input=${(e: Event) =>
1238
+ props.onFormChange({
1239
+ failureAlertCooldownSeconds: (e.target as HTMLInputElement)
1240
+ .value,
1241
+ })}
1242
+ placeholder="3600"
1243
+ />
1244
+ <div class="cron-help">Minimum seconds between alerts.</div>
1245
+ ${renderFieldError(
1246
+ props.fieldErrors.failureAlertCooldownSeconds,
1247
+ errorIdForField("failureAlertCooldownSeconds"),
1248
+ )}
1249
+ </label>
1250
+ <label class="field">
1251
+ ${renderFieldLabel("Alert channel")}
1252
+ <select
1253
+ .value=${props.form.failureAlertChannel || "last"}
1254
+ @change=${(e: Event) =>
1255
+ props.onFormChange({
1256
+ failureAlertChannel: (e.target as HTMLSelectElement).value,
1257
+ })}
1258
+ >
1259
+ ${channelOptions.map(
1260
+ (channel) =>
1261
+ html`<option value=${channel}>
1262
+ ${resolveChannelLabel(props, channel)}
1263
+ </option>`,
1264
+ )}
1265
+ </select>
1266
+ </label>
1267
+ <label class="field">
1268
+ ${renderFieldLabel("Alert to")}
1269
+ <input
1270
+ .value=${props.form.failureAlertTo}
1271
+ list="cron-delivery-to-suggestions"
1272
+ @input=${(e: Event) =>
1273
+ props.onFormChange({
1274
+ failureAlertTo: (e.target as HTMLInputElement).value,
1275
+ })}
1276
+ placeholder="+1555... or chat id"
1277
+ />
1278
+ <div class="cron-help">
1279
+ Optional recipient override for failure alerts.
1280
+ </div>
1281
+ </label>
1282
+ <label class="field">
1283
+ ${renderFieldLabel("Alert mode")}
1284
+ <select
1285
+ .value=${props.form.failureAlertDeliveryMode || "announce"}
1286
+ @change=${(e: Event) =>
1287
+ props.onFormChange({
1288
+ failureAlertDeliveryMode: (e.target as HTMLSelectElement)
1289
+ .value as CronFormState["failureAlertDeliveryMode"],
1290
+ })}
1291
+ >
1292
+ <option value="announce">Announce (via channel)</option>
1293
+ <option value="webhook">Webhook (HTTP POST)</option>
1294
+ </select>
1295
+ </label>
1296
+ <label class="field">
1297
+ ${renderFieldLabel("Alert account ID")}
1298
+ <input
1299
+ .value=${props.form.failureAlertAccountId}
1300
+ @input=${(e: Event) =>
1301
+ props.onFormChange({
1302
+ failureAlertAccountId: (e.target as HTMLInputElement).value,
1303
+ })}
1304
+ placeholder="Account ID for multi-account setups"
1305
+ />
1306
+ </label>
1307
+ `
1308
+ : nothing
1309
+ }
1310
+ `
1311
+ : nothing
1312
+ }
1313
+ ${
1314
+ selectedDeliveryMode !== "none"
1315
+ ? html`
1316
+ <label class="field checkbox cron-checkbox cron-span-2">
1317
+ <input
1318
+ type="checkbox"
1319
+ .checked=${props.form.deliveryBestEffort}
1320
+ @change=${(e: Event) =>
1321
+ props.onFormChange({
1322
+ deliveryBestEffort: (e.target as HTMLInputElement).checked,
1323
+ })}
1324
+ />
1325
+ <span class="field-checkbox__label">${t("cron.form.bestEffortDelivery")}</span>
1326
+ <div class="cron-help">${t("cron.form.bestEffortHelp")}</div>
1327
+ </label>
1328
+ `
1329
+ : nothing
1330
+ }
1331
+ </div>
1332
+ </details>
1333
+ </div>
1334
+ ${
1335
+ blockedByValidation
1336
+ ? html`
1337
+ <div class="cron-form-status" role="status" aria-live="polite">
1338
+ <div class="cron-form-status__title">${t("cron.form.cantAddYet")}</div>
1339
+ <div class="cron-help">${t("cron.form.fillRequired")}</div>
1340
+ <ul class="cron-form-status__list">
1341
+ ${blockingFields.map(
1342
+ (field) => html`
1343
+ <li>
1344
+ <button
1345
+ type="button"
1346
+ class="cron-form-status__link"
1347
+ @click=${() => focusFormField(field.inputId)}
1348
+ >
1349
+ ${field.label}: ${t(field.message)}
1350
+ </button>
1351
+ </li>
1352
+ `,
1353
+ )}
1354
+ </ul>
1355
+ </div>
1356
+ `
1357
+ : nothing
1358
+ }
1359
+ <div class="row cron-form-actions">
1360
+ <button class="btn primary" ?disabled=${props.busy || !props.canSubmit} @click=${props.onAdd}>
1361
+ ${props.busy ? t("cron.form.saving") : isEditing ? t("cron.form.saveChanges") : t("cron.form.addJob")}
1362
+ </button>
1363
+ ${
1364
+ submitDisabledReason
1365
+ ? html`<div class="cron-submit-reason" aria-live="polite">${submitDisabledReason}</div>`
1366
+ : nothing
1367
+ }
1368
+ ${
1369
+ isEditing
1370
+ ? html`
1371
+ <button class="btn" ?disabled=${props.busy} @click=${props.onCancelEdit}>
1372
+ ${t("cron.form.cancel")}
1373
+ </button>
1374
+ `
1375
+ : nothing
1376
+ }
1377
+ </div>
1378
+ </section>
1379
+ </section>
1380
+
1381
+ ${renderSuggestionList("cron-agent-suggestions", props.agentSuggestions)}
1382
+ ${renderSuggestionList("cron-model-suggestions", props.modelSuggestions)}
1383
+ ${renderSuggestionList("cron-thinking-suggestions", props.thinkingSuggestions)}
1384
+ ${renderSuggestionList("cron-tz-suggestions", props.timezoneSuggestions)}
1385
+ ${renderSuggestionList("cron-delivery-to-suggestions", props.deliveryToSuggestions)}
1386
+ ${renderSuggestionList("cron-delivery-account-suggestions", props.accountSuggestions)}
1387
+ `;
1388
+ }
1389
+
1390
+ function renderScheduleFields(props: CronProps) {
1391
+ const form = props.form;
1392
+ if (form.scheduleKind === "at") {
1393
+ return html`
1394
+ <label class="field cron-span-2" style="margin-top: 12px;">
1395
+ ${renderFieldLabel(t("cron.form.runAt"), true)}
1396
+ <input
1397
+ id="cron-schedule-at"
1398
+ type="datetime-local"
1399
+ .value=${form.scheduleAt}
1400
+ aria-invalid=${props.fieldErrors.scheduleAt ? "true" : "false"}
1401
+ aria-describedby=${ifDefined(
1402
+ props.fieldErrors.scheduleAt ? errorIdForField("scheduleAt") : undefined,
1403
+ )}
1404
+ @input=${(e: Event) =>
1405
+ props.onFormChange({
1406
+ scheduleAt: (e.target as HTMLInputElement).value,
1407
+ })}
1408
+ />
1409
+ ${renderFieldError(props.fieldErrors.scheduleAt, errorIdForField("scheduleAt"))}
1410
+ </label>
1411
+ `;
1412
+ }
1413
+ if (form.scheduleKind === "every") {
1414
+ return html`
1415
+ <div class="form-grid cron-form-grid" style="margin-top: 12px;">
1416
+ <label class="field">
1417
+ ${renderFieldLabel(t("cron.form.every"), true)}
1418
+ <input
1419
+ id="cron-every-amount"
1420
+ .value=${form.everyAmount}
1421
+ aria-invalid=${props.fieldErrors.everyAmount ? "true" : "false"}
1422
+ aria-describedby=${ifDefined(
1423
+ props.fieldErrors.everyAmount ? errorIdForField("everyAmount") : undefined,
1424
+ )}
1425
+ @input=${(e: Event) =>
1426
+ props.onFormChange({
1427
+ everyAmount: (e.target as HTMLInputElement).value,
1428
+ })}
1429
+ placeholder=${t("cron.form.everyAmountPlaceholder")}
1430
+ />
1431
+ ${renderFieldError(props.fieldErrors.everyAmount, errorIdForField("everyAmount"))}
1432
+ </label>
1433
+ <label class="field">
1434
+ <span>${t("cron.form.unit")}</span>
1435
+ <select
1436
+ .value=${form.everyUnit}
1437
+ @change=${(e: Event) =>
1438
+ props.onFormChange({
1439
+ everyUnit: (e.target as HTMLSelectElement).value as CronFormState["everyUnit"],
1440
+ })}
1441
+ >
1442
+ <option value="minutes">${t("cron.form.minutes")}</option>
1443
+ <option value="hours">${t("cron.form.hours")}</option>
1444
+ <option value="days">${t("cron.form.days")}</option>
1445
+ </select>
1446
+ </label>
1447
+ </div>
1448
+ `;
1449
+ }
1450
+ return html`
1451
+ <div class="form-grid cron-form-grid" style="margin-top: 12px;">
1452
+ <label class="field">
1453
+ ${renderFieldLabel(t("cron.form.expression"), true)}
1454
+ <input
1455
+ id="cron-cron-expr"
1456
+ .value=${form.cronExpr}
1457
+ aria-invalid=${props.fieldErrors.cronExpr ? "true" : "false"}
1458
+ aria-describedby=${ifDefined(
1459
+ props.fieldErrors.cronExpr ? errorIdForField("cronExpr") : undefined,
1460
+ )}
1461
+ @input=${(e: Event) =>
1462
+ props.onFormChange({ cronExpr: (e.target as HTMLInputElement).value })}
1463
+ placeholder=${t("cron.form.expressionPlaceholder")}
1464
+ />
1465
+ ${renderFieldError(props.fieldErrors.cronExpr, errorIdForField("cronExpr"))}
1466
+ </label>
1467
+ <label class="field">
1468
+ <span>${t("cron.form.timezoneOptional")}</span>
1469
+ <input
1470
+ .value=${form.cronTz}
1471
+ list="cron-tz-suggestions"
1472
+ @input=${(e: Event) =>
1473
+ props.onFormChange({ cronTz: (e.target as HTMLInputElement).value })}
1474
+ placeholder=${t("cron.form.timezonePlaceholder")}
1475
+ />
1476
+ <div class="cron-help">${t("cron.form.timezoneHelp")}</div>
1477
+ </label>
1478
+ <div class="cron-help cron-span-2">${t("cron.form.jitterHelp")}</div>
1479
+ </div>
1480
+ `;
1481
+ }
1482
+
1483
+ function renderFieldError(message?: string, id?: string) {
1484
+ if (!message) {
1485
+ return nothing;
1486
+ }
1487
+ return html`<div id=${ifDefined(id)} class="cron-help cron-error">${t(message)}</div>`;
1488
+ }
1489
+
1490
+ function renderJob(job: CronJob, props: CronProps) {
1491
+ const isSelected = props.runsJobId === job.id;
1492
+ const itemClass = `list-item list-item-clickable cron-job${isSelected ? " list-item-selected" : ""}`;
1493
+ const selectAnd = (action: () => void) => {
1494
+ props.onLoadRuns(job.id);
1495
+ action();
1496
+ };
1497
+ return html`
1498
+ <div class=${itemClass} @click=${() => props.onLoadRuns(job.id)}>
1499
+ <div class="list-main">
1500
+ <div class="list-title">${job.name}</div>
1501
+ <div class="list-sub">${formatCronSchedule(job)}</div>
1502
+ ${renderJobPayload(job)}
1503
+ ${job.agentId ? html`<div class="muted cron-job-agent">${t("cron.jobDetail.agent")}: ${job.agentId}</div>` : nothing}
1504
+ </div>
1505
+ <div class="list-meta">
1506
+ ${renderJobState(job)}
1507
+ </div>
1508
+ <div class="cron-job-footer">
1509
+ <div class="chip-row cron-job-chips">
1510
+ <span class=${`chip ${job.enabled ? "chip-ok" : "chip-danger"}`}>
1511
+ ${job.enabled ? t("cron.jobList.enabled") : t("cron.jobList.disabled")}
1512
+ </span>
1513
+ <span class="chip">${job.sessionTarget}</span>
1514
+ <span class="chip">${job.wakeMode}</span>
1515
+ </div>
1516
+ <div class="row cron-job-actions">
1517
+ <button
1518
+ class="btn"
1519
+ ?disabled=${props.busy}
1520
+ @click=${(event: Event) => {
1521
+ event.stopPropagation();
1522
+ selectAnd(() => props.onEdit(job));
1523
+ }}
1524
+ >
1525
+ ${t("cron.jobList.edit")}
1526
+ </button>
1527
+ <button
1528
+ class="btn"
1529
+ ?disabled=${props.busy}
1530
+ @click=${(event: Event) => {
1531
+ event.stopPropagation();
1532
+ selectAnd(() => props.onClone(job));
1533
+ }}
1534
+ >
1535
+ ${t("cron.jobList.clone")}
1536
+ </button>
1537
+ <button
1538
+ class="btn"
1539
+ ?disabled=${props.busy}
1540
+ @click=${(event: Event) => {
1541
+ event.stopPropagation();
1542
+ selectAnd(() => props.onToggle(job, !job.enabled));
1543
+ }}
1544
+ >
1545
+ ${job.enabled ? t("cron.jobList.disable") : t("cron.jobList.enable")}
1546
+ </button>
1547
+ <button
1548
+ class="btn"
1549
+ ?disabled=${props.busy}
1550
+ @click=${(event: Event) => {
1551
+ event.stopPropagation();
1552
+ selectAnd(() => props.onRun(job, "force"));
1553
+ }}
1554
+ >
1555
+ ${t("cron.jobList.run")}
1556
+ </button>
1557
+ <button
1558
+ class="btn"
1559
+ ?disabled=${props.busy}
1560
+ @click=${(event: Event) => {
1561
+ event.stopPropagation();
1562
+ selectAnd(() => props.onRun(job, "due"));
1563
+ }}
1564
+ >
1565
+ Run if due
1566
+ </button>
1567
+ <button
1568
+ class="btn"
1569
+ ?disabled=${props.busy}
1570
+ @click=${(event: Event) => {
1571
+ event.stopPropagation();
1572
+ selectAnd(() => props.onLoadRuns(job.id));
1573
+ }}
1574
+ >
1575
+ ${t("cron.jobList.history")}
1576
+ </button>
1577
+ <button
1578
+ class="btn danger"
1579
+ ?disabled=${props.busy}
1580
+ @click=${(event: Event) => {
1581
+ event.stopPropagation();
1582
+ selectAnd(() => props.onRemove(job));
1583
+ }}
1584
+ >
1585
+ ${t("cron.jobList.remove")}
1586
+ </button>
1587
+ </div>
1588
+ </div>
1589
+ </div>
1590
+ `;
1591
+ }
1592
+
1593
+ function renderJobPayload(job: CronJob) {
1594
+ if (job.payload.kind === "systemEvent") {
1595
+ return html`<div class="cron-job-detail">
1596
+ <span class="cron-job-detail-label">${t("cron.jobDetail.system")}</span>
1597
+ <span class="muted cron-job-detail-value">${job.payload.text}</span>
1598
+ </div>`;
1599
+ }
1600
+
1601
+ const delivery = job.delivery;
1602
+ const deliveryTarget =
1603
+ delivery?.mode === "webhook"
1604
+ ? delivery.to
1605
+ ? ` (${delivery.to})`
1606
+ : ""
1607
+ : delivery?.channel || delivery?.to
1608
+ ? ` (${delivery.channel ?? "last"}${delivery.to ? ` -> ${delivery.to}` : ""})`
1609
+ : "";
1610
+
1611
+ return html`
1612
+ <div class="cron-job-detail">
1613
+ <span class="cron-job-detail-label">${t("cron.jobDetail.prompt")}</span>
1614
+ <span class="muted cron-job-detail-value">${job.payload.message}</span>
1615
+ </div>
1616
+ ${
1617
+ delivery
1618
+ ? html`<div class="cron-job-detail">
1619
+ <span class="cron-job-detail-label">${t("cron.jobDetail.delivery")}</span>
1620
+ <span class="muted cron-job-detail-value">${delivery.mode}${deliveryTarget}</span>
1621
+ </div>`
1622
+ : nothing
1623
+ }
1624
+ `;
1625
+ }
1626
+
1627
+ function formatStateRelative(ms?: number) {
1628
+ if (typeof ms !== "number" || !Number.isFinite(ms)) {
1629
+ return t("common.na");
1630
+ }
1631
+ return formatRelativeTimestamp(ms);
1632
+ }
1633
+
1634
+ function formatRunNextLabel(nextRunAtMs: number, nowMs = Date.now()) {
1635
+ const rel = formatRelativeTimestamp(nextRunAtMs);
1636
+ return nextRunAtMs > nowMs ? t("cron.runEntry.next", { rel }) : t("cron.runEntry.due", { rel });
1637
+ }
1638
+
1639
+ function renderJobState(job: CronJob) {
1640
+ const rawStatus = job.state?.lastStatus;
1641
+ const statusClass =
1642
+ rawStatus === "ok"
1643
+ ? "cron-job-status-ok"
1644
+ : rawStatus === "error"
1645
+ ? "cron-job-status-error"
1646
+ : rawStatus === "skipped"
1647
+ ? "cron-job-status-skipped"
1648
+ : "cron-job-status-na";
1649
+ const statusLabel =
1650
+ rawStatus === "ok"
1651
+ ? t("cron.runs.runStatusOk")
1652
+ : rawStatus === "error"
1653
+ ? t("cron.runs.runStatusError")
1654
+ : rawStatus === "skipped"
1655
+ ? t("cron.runs.runStatusSkipped")
1656
+ : t("common.na");
1657
+ const nextRunAtMs = job.state?.nextRunAtMs;
1658
+ const lastRunAtMs = job.state?.lastRunAtMs;
1659
+
1660
+ return html`
1661
+ <div class="cron-job-state">
1662
+ <div class="cron-job-state-row">
1663
+ <span class="cron-job-state-key">${t("cron.jobState.status")}</span>
1664
+ <span class=${`cron-job-status-pill ${statusClass}`}>${statusLabel}</span>
1665
+ </div>
1666
+ <div class="cron-job-state-row">
1667
+ <span class="cron-job-state-key">${t("cron.jobState.next")}</span>
1668
+ <span class="cron-job-state-value" title=${formatMs(nextRunAtMs)}>
1669
+ ${formatStateRelative(nextRunAtMs)}
1670
+ </span>
1671
+ </div>
1672
+ <div class="cron-job-state-row">
1673
+ <span class="cron-job-state-key">${t("cron.jobState.last")}</span>
1674
+ <span class="cron-job-state-value" title=${formatMs(lastRunAtMs)}>
1675
+ ${formatStateRelative(lastRunAtMs)}
1676
+ </span>
1677
+ </div>
1678
+ </div>
1679
+ `;
1680
+ }
1681
+
1682
+ function runStatusLabel(value: string): string {
1683
+ switch (value) {
1684
+ case "ok":
1685
+ return t("cron.runs.runStatusOk");
1686
+ case "error":
1687
+ return t("cron.runs.runStatusError");
1688
+ case "skipped":
1689
+ return t("cron.runs.runStatusSkipped");
1690
+ default:
1691
+ return t("cron.runs.runStatusUnknown");
1692
+ }
1693
+ }
1694
+
1695
+ function runDeliveryLabel(value: string): string {
1696
+ switch (value) {
1697
+ case "delivered":
1698
+ return t("cron.runs.deliveryDelivered");
1699
+ case "not-delivered":
1700
+ return t("cron.runs.deliveryNotDelivered");
1701
+ case "not-requested":
1702
+ return t("cron.runs.deliveryNotRequested");
1703
+ case "unknown":
1704
+ return t("cron.runs.deliveryUnknown");
1705
+ default:
1706
+ return t("cron.runs.deliveryUnknown");
1707
+ }
1708
+ }
1709
+
1710
+ function renderRun(entry: CronRunLogEntry, basePath: string) {
1711
+ const chatUrl =
1712
+ typeof entry.sessionKey === "string" && entry.sessionKey.trim().length > 0
1713
+ ? `${pathForTab("chat", basePath)}?session=${encodeURIComponent(entry.sessionKey)}`
1714
+ : null;
1715
+ const status = runStatusLabel(entry.status ?? "unknown");
1716
+ const delivery = runDeliveryLabel(entry.deliveryStatus ?? "not-requested");
1717
+ const usage = entry.usage;
1718
+ const usageSummary =
1719
+ usage && typeof usage.total_tokens === "number"
1720
+ ? `${usage.total_tokens} tokens`
1721
+ : usage && typeof usage.input_tokens === "number" && typeof usage.output_tokens === "number"
1722
+ ? `${usage.input_tokens} in / ${usage.output_tokens} out`
1723
+ : null;
1724
+ return html`
1725
+ <div class="list-item cron-run-entry">
1726
+ <div class="list-main cron-run-entry__main">
1727
+ <div class="list-title cron-run-entry__title">
1728
+ ${entry.jobName ?? entry.jobId}
1729
+ <span class="muted"> · ${status}</span>
1730
+ </div>
1731
+ <div class="list-sub cron-run-entry__summary">${entry.summary ?? entry.error ?? t("cron.runEntry.noSummary")}</div>
1732
+ <div class="chip-row" style="margin-top: 6px;">
1733
+ <span class="chip">${delivery}</span>
1734
+ ${entry.model ? html`<span class="chip">${entry.model}</span>` : nothing}
1735
+ ${entry.provider ? html`<span class="chip">${entry.provider}</span>` : nothing}
1736
+ ${usageSummary ? html`<span class="chip">${usageSummary}</span>` : nothing}
1737
+ </div>
1738
+ </div>
1739
+ <div class="list-meta cron-run-entry__meta">
1740
+ <div>${formatMs(entry.ts)}</div>
1741
+ ${typeof entry.runAtMs === "number" ? html`<div class="muted">${t("cron.runEntry.runAt")} ${formatMs(entry.runAtMs)}</div>` : nothing}
1742
+ <div class="muted">${entry.durationMs ?? 0}ms</div>
1743
+ ${
1744
+ typeof entry.nextRunAtMs === "number"
1745
+ ? html`<div class="muted">${formatRunNextLabel(entry.nextRunAtMs)}</div>`
1746
+ : nothing
1747
+ }
1748
+ ${
1749
+ chatUrl
1750
+ ? html`<div><a class="session-link" href=${chatUrl}>${t("cron.runEntry.openRunChat")}</a></div>`
1751
+ : nothing
1752
+ }
1753
+ ${entry.error ? html`<div class="muted">${entry.error}</div>` : nothing}
1754
+ ${entry.deliveryError ? html`<div class="muted">${entry.deliveryError}</div>` : nothing}
1755
+ </div>
1756
+ </div>
1757
+ `;
1758
+ }