@polderlabs/bizar 3.23.0 → 4.0.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 (346) hide show
  1. package/README.md +72 -2
  2. package/bizar-dash/.bizar/activity.log +3 -0
  3. package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
  4. package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
  5. package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
  6. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
  7. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
  8. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
  9. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
  10. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
  11. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
  12. package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
  13. package/bizar-dash/.bizar/graph/graph.html +307 -0
  14. package/bizar-dash/.bizar/graph/graph.json +40267 -0
  15. package/bizar-dash/.bizar/graph/manifest.json +762 -0
  16. package/bizar-dash/.graphifyignore +40 -0
  17. package/bizar-dash/.obsidian/README.md +14 -0
  18. package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
  19. package/bizar-dash/.obsidian/vault.json +7 -0
  20. package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
  21. package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
  22. package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
  23. package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
  24. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
  25. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
  26. package/bizar-dash/CHANGELOG.md +474 -0
  27. package/bizar-dash/DESIGN.md +136 -0
  28. package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
  29. package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
  30. package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
  31. package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
  32. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
  33. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
  34. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
  35. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
  36. package/bizar-dash/dist/index.html +40 -0
  37. package/bizar-dash/dist/mobile.html +22 -0
  38. package/bizar-dash/plans/debug-plan/comments.json +1 -0
  39. package/bizar-dash/plans/debug-plan/meta.json +10 -0
  40. package/bizar-dash/plans/debug-plan/plan.json +174 -0
  41. package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
  42. package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
  43. package/bizar-dash/src/cli.mjs +522 -0
  44. package/bizar-dash/src/server/activity-log.mjs +174 -0
  45. package/bizar-dash/src/server/agents-store.mjs +465 -0
  46. package/bizar-dash/src/server/api.mjs +138 -0
  47. package/bizar-dash/src/server/artifact-template.html +810 -0
  48. package/bizar-dash/src/server/artifacts-store.mjs +733 -0
  49. package/bizar-dash/src/server/auth.mjs +381 -0
  50. package/bizar-dash/src/server/background-store.mjs +447 -0
  51. package/bizar-dash/src/server/bg-poller.mjs +305 -0
  52. package/bizar-dash/src/server/bg-retry.mjs +574 -0
  53. package/bizar-dash/src/server/browser.mjs +40 -0
  54. package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
  55. package/bizar-dash/src/server/dialog-poller.mjs +105 -0
  56. package/bizar-dash/src/server/dialog-store.mjs +177 -0
  57. package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
  58. package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
  59. package/bizar-dash/src/server/memory-git.mjs +249 -0
  60. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  61. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  62. package/bizar-dash/src/server/memory-store.mjs +424 -0
  63. package/bizar-dash/src/server/mod-security.mjs +398 -0
  64. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  65. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  66. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  67. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  68. package/bizar-dash/src/server/pair-store.mjs +138 -0
  69. package/bizar-dash/src/server/projects-store.mjs +364 -0
  70. package/bizar-dash/src/server/providers-store.mjs +789 -0
  71. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  72. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  73. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  74. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  75. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  76. package/bizar-dash/src/server/routes/background.mjs +165 -0
  77. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  78. package/bizar-dash/src/server/routes/config.mjs +129 -0
  79. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  80. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  81. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  82. package/bizar-dash/src/server/routes/history.mjs +78 -0
  83. package/bizar-dash/src/server/routes/memory.mjs +499 -0
  84. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  85. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  86. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  87. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  88. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  89. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  90. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  91. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  92. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  93. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  94. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  95. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  96. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  97. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  98. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  99. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  100. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  101. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  102. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  103. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  104. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  105. package/bizar-dash/src/server/search-store.mjs +220 -0
  106. package/bizar-dash/src/server/serve-info.mjs +684 -0
  107. package/bizar-dash/src/server/server.mjs +796 -0
  108. package/bizar-dash/src/server/settings-store.mjs +45 -0
  109. package/bizar-dash/src/server/skills-store.mjs +300 -0
  110. package/bizar-dash/src/server/state.mjs +437 -0
  111. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  112. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  113. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  114. package/bizar-dash/src/server/tui.mjs +923 -0
  115. package/bizar-dash/src/server/update-store.mjs +168 -0
  116. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  117. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  118. package/bizar-dash/src/server/watcher.mjs +81 -0
  119. package/bizar-dash/src/server/yaml.mjs +238 -0
  120. package/bizar-dash/src/web/App.tsx +718 -0
  121. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  122. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  123. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  124. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  125. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  126. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  127. package/bizar-dash/src/web/components/Button.tsx +56 -0
  128. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  129. package/bizar-dash/src/web/components/Card.tsx +40 -0
  130. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  131. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  132. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  133. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  134. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  135. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  136. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  137. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  138. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  139. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  140. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  141. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  142. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  143. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  144. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  145. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  146. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  147. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  148. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  149. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  150. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  151. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  152. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  153. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  154. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  155. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  156. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  157. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  158. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  159. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  160. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  161. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  162. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  163. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  164. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  165. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  166. package/bizar-dash/src/web/index.html +37 -0
  167. package/bizar-dash/src/web/lib/api.ts +193 -0
  168. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  169. package/bizar-dash/src/web/lib/types.ts +668 -0
  170. package/bizar-dash/src/web/lib/utils.ts +114 -0
  171. package/bizar-dash/src/web/lib/ws.ts +176 -0
  172. package/bizar-dash/src/web/main.tsx +34 -0
  173. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  174. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  175. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  176. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  177. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  178. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  179. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  180. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  181. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  183. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  184. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  185. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  186. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  188. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  189. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  190. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  194. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  195. package/bizar-dash/src/web/mobile.html +20 -0
  196. package/bizar-dash/src/web/mobile.tsx +13 -0
  197. package/bizar-dash/src/web/styles/chat.css +198 -0
  198. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  199. package/bizar-dash/src/web/styles/main.css +8855 -0
  200. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  201. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  202. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  203. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  204. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  205. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  206. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  207. package/bizar-dash/src/web/views/Config.tsx +1603 -0
  208. package/bizar-dash/src/web/views/History.tsx +335 -0
  209. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  210. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  211. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  212. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  213. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  214. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  215. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  216. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  217. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  218. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  219. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  220. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  221. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  222. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  223. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  224. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  225. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  226. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  227. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  228. package/bizar-dash/tests/memory-git.test.mjs +138 -0
  229. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  230. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  231. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  232. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  233. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  234. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  235. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  236. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  237. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  238. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  239. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  240. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  241. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  242. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  243. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  244. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  245. package/bizar-dash/tests/yaml.test.mjs +128 -0
  246. package/bizar-dash/tsconfig.json +23 -0
  247. package/bizar-dash/vite.config.ts +27 -0
  248. package/cli/atomic.mjs +73 -0
  249. package/cli/bin.mjs +43 -46
  250. package/cli/copy.mjs +3 -0
  251. package/cli/dev-link.mjs +17 -1
  252. package/cli/init.mjs +135 -1
  253. package/cli/install.mjs +10 -23
  254. package/cli/memory-constants.mjs +59 -0
  255. package/cli/memory.mjs +757 -0
  256. package/cli/service.mjs +5 -6
  257. package/config/skills/obsidian/SKILL.md +213 -65
  258. package/install.sh +48 -18
  259. package/package.json +49 -14
  260. package/packages/sdk/LICENSE +21 -0
  261. package/packages/sdk/README.md +80 -0
  262. package/packages/sdk/src/client.ts +183 -0
  263. package/packages/sdk/src/errors.ts +120 -0
  264. package/packages/sdk/src/events.ts +153 -0
  265. package/packages/sdk/src/index.ts +63 -0
  266. package/packages/sdk/src/types.ts +176 -0
  267. package/packages/sdk/src/version.ts +4 -0
  268. package/packages/sdk/tests/client.test.ts +217 -0
  269. package/packages/sdk/tests/errors.test.ts +108 -0
  270. package/packages/sdk/tests/events.test.ts +139 -0
  271. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  272. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  273. package/packages/sdk/tsconfig.json +25 -0
  274. package/packages/sdk/vitest.config.ts +9 -0
  275. package/plugins/bizar/LICENSE +21 -0
  276. package/plugins/bizar/README.md +448 -0
  277. package/plugins/bizar/dist/index.js +29901 -0
  278. package/plugins/bizar/index.ts +1484 -0
  279. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  280. package/plugins/bizar/src/background-state.ts +551 -0
  281. package/plugins/bizar/src/background.ts +1250 -0
  282. package/plugins/bizar/src/commands-impl.ts +464 -0
  283. package/plugins/bizar/src/commands.ts +1127 -0
  284. package/plugins/bizar/src/dashboard-client.ts +233 -0
  285. package/plugins/bizar/src/event-stream.ts +606 -0
  286. package/plugins/bizar/src/fingerprint.ts +120 -0
  287. package/plugins/bizar/src/handoff.ts +79 -0
  288. package/plugins/bizar/src/http-client.ts +467 -0
  289. package/plugins/bizar/src/key-rotation.ts +218 -0
  290. package/plugins/bizar/src/logger.ts +144 -0
  291. package/plugins/bizar/src/loop.ts +176 -0
  292. package/plugins/bizar/src/opencode-runner.ts +390 -0
  293. package/plugins/bizar/src/options.ts +421 -0
  294. package/plugins/bizar/src/plan-fs.ts +323 -0
  295. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  296. package/plugins/bizar/src/report.ts +178 -0
  297. package/plugins/bizar/src/research-prompt.ts +35 -0
  298. package/plugins/bizar/src/serve-info.ts +228 -0
  299. package/plugins/bizar/src/serve.ts +496 -0
  300. package/plugins/bizar/src/settings.ts +349 -0
  301. package/plugins/bizar/src/state.ts +298 -0
  302. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  303. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  304. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  305. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  306. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  307. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  308. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  309. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  310. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  311. package/plugins/bizar/tests/background-state.test.ts +277 -0
  312. package/plugins/bizar/tests/background.test.ts +402 -0
  313. package/plugins/bizar/tests/block.test.ts +195 -0
  314. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  315. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  316. package/plugins/bizar/tests/commands.test.ts +548 -0
  317. package/plugins/bizar/tests/config.test.ts +122 -0
  318. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  319. package/plugins/bizar/tests/dispose.test.ts +336 -0
  320. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  321. package/plugins/bizar/tests/event.test.ts +262 -0
  322. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  323. package/plugins/bizar/tests/http-client.test.ts +404 -0
  324. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  325. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  326. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  327. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  328. package/plugins/bizar/tests/loop.test.ts +397 -0
  329. package/plugins/bizar/tests/options.test.ts +276 -0
  330. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  331. package/plugins/bizar/tests/serve.test.ts +339 -0
  332. package/plugins/bizar/tests/settings.test.ts +351 -0
  333. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  334. package/plugins/bizar/tests/state.test.ts +276 -0
  335. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  336. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  337. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  338. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  339. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  340. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  341. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  342. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  343. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  344. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  345. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  346. package/plugins/bizar/tsconfig.json +29 -0
@@ -0,0 +1,685 @@
1
+ // src/mobile/views/MobileSchedules.tsx — schedules list with detail sheet, edit modal, and a reusable New/Edit modal.
2
+ //
3
+ // v3.9.0 — overhaul:
4
+ // - PATCH for partial updates (toggle enabled, edit fields) is now
5
+ // supported by the backend (see routes/schedules.mjs).
6
+ // - NewScheduleModal accepts an optional `initial` schedule and submits
7
+ // PUT when editing, POST when creating. The same modal handles both
8
+ // flows so the operator doesn't context-switch between forms.
9
+ // - Detail sheet exposes an Edit button so the mobile UI can edit any
10
+ // schedule, not just toggle it.
11
+ // - The structured pickers mirror the desktop (DOW, hour, minute,
12
+ // timezone) but in a more compact layout suited to a phone screen.
13
+ import { useEffect, useState } from 'react';
14
+ import { Clock, Plus, RefreshCw, Trash2, Play, Pencil } from 'lucide-react';
15
+ import { api } from '../../lib/api';
16
+ import type { Schedule, Snapshot } from '../../lib/types';
17
+ import { MobileBottomSheet } from '../components/MobileBottomSheet';
18
+ import { MobileModal } from '../components/MobileModal';
19
+
20
+ type Props = {
21
+ snapshot: Snapshot;
22
+ onBack: () => void;
23
+ };
24
+
25
+ const TYPES: Array<Schedule['type']> = ['cron', 'interval', 'once'];
26
+ const ACTIONS: Array<Schedule['action']['type']> = ['command', 'agent', 'webhook'];
27
+
28
+ const TIMEZONES = ['UTC', 'America/New_York', 'America/Los_Angeles', 'Europe/London', 'Europe/Berlin', 'Asia/Tokyo'];
29
+
30
+ const HOURS = Array.from({ length: 24 }, (_, h) => ({
31
+ value: h,
32
+ label: h === 0 ? '12 AM' : h < 12 ? `${h} AM` : h === 12 ? '12 PM' : `${h - 12} PM`,
33
+ }));
34
+ const MINUTES = Array.from({ length: 12 }, (_, i) => i * 5); // 0,5,…,55
35
+ const DOWS = [
36
+ { value: '*', label: 'Every day' },
37
+ { value: '0', label: 'Sun' },
38
+ { value: '1', label: 'Mon' },
39
+ { value: '2', label: 'Tue' },
40
+ { value: '3', label: 'Wed' },
41
+ { value: '4', label: 'Thu' },
42
+ { value: '5', label: 'Fri' },
43
+ { value: '6', label: 'Sat' },
44
+ ];
45
+ const INTERVAL_UNITS = [
46
+ { value: 's', label: 'sec' },
47
+ { value: 'm', label: 'min' },
48
+ { value: 'h', label: 'hr' },
49
+ { value: 'd', label: 'day' },
50
+ ];
51
+
52
+ export function MobileSchedules({ snapshot }: Props) {
53
+ const [schedules, setSchedules] = useState<Schedule[]>(snapshot.schedules || []);
54
+ const [loading, setLoading] = useState(!snapshot.schedules);
55
+ const [filter, setFilter] = useState('');
56
+ const [detailSchedule, setDetailSchedule] = useState<Schedule | null>(null);
57
+ const [editor, setEditor] = useState<{ open: boolean; initial?: Schedule }>({ open: false });
58
+
59
+ const reload = async () => {
60
+ try {
61
+ const data = await api.get<{ schedules: Schedule[] }>('/schedules');
62
+ setSchedules(data.schedules || []);
63
+ } catch {
64
+ // best-effort
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
69
+
70
+ useEffect(() => {
71
+ if (snapshot.schedules) {
72
+ setSchedules(snapshot.schedules);
73
+ setLoading(false);
74
+ } else {
75
+ reload();
76
+ }
77
+ }, [snapshot.schedules]);
78
+
79
+ // v3.9.0 — PATCH partial update (toggle enabled, edit name, etc.).
80
+ // The backend supports PATCH (see routes/schedules.mjs).
81
+ const toggleSchedule = async (id: string, enabled: boolean) => {
82
+ try {
83
+ await api.patch(`/schedules/${encodeURIComponent(id)}`, { enabled });
84
+ setSchedules((cur) => cur.map((s) => (s.id === id ? { ...s, enabled } : s)));
85
+ if (detailSchedule?.id === id) {
86
+ setDetailSchedule((s) => (s ? { ...s, enabled } : s));
87
+ }
88
+ } catch {
89
+ // best-effort
90
+ }
91
+ };
92
+
93
+ const deleteSchedule = async (id: string) => {
94
+ if (!confirm('Delete this schedule?')) return;
95
+ try {
96
+ await api.del(`/schedules/${encodeURIComponent(id)}`);
97
+ setSchedules((cur) => cur.filter((s) => s.id !== id));
98
+ if (detailSchedule?.id === id) setDetailSchedule(null);
99
+ } catch {
100
+ // best-effort
101
+ }
102
+ };
103
+
104
+ // v3.9.0 — backend added /trigger as an alias of /run (mobile uses this).
105
+ const triggerSchedule = async (id: string) => {
106
+ try {
107
+ await api.post(`/schedules/${encodeURIComponent(id)}/trigger`);
108
+ } catch {
109
+ // best-effort
110
+ }
111
+ };
112
+
113
+ const filtered = filter.trim()
114
+ ? schedules.filter((s) => s.name.toLowerCase().includes(filter.toLowerCase()))
115
+ : schedules;
116
+
117
+ return (
118
+ <div className="mobile-view">
119
+ <div className="mobile-tasks-toolbar">
120
+ <input
121
+ className="mobile-search-input"
122
+ type="text"
123
+ placeholder="Search schedules…"
124
+ value={filter}
125
+ onChange={(e) => setFilter(e.target.value)}
126
+ style={{ flex: 1 }}
127
+ />
128
+ <button type="button" className="mobile-icon-btn" onClick={() => reload()} aria-label="Refresh">
129
+ <RefreshCw size={16} />
130
+ </button>
131
+ <button
132
+ type="button"
133
+ className="mobile-icon-btn"
134
+ onClick={() => setEditor({ open: true })}
135
+ aria-label="New schedule"
136
+ >
137
+ <Plus size={16} />
138
+ </button>
139
+ </div>
140
+
141
+ {loading ? (
142
+ <div className="mobile-loading"><p>Loading…</p></div>
143
+ ) : filtered.length === 0 ? (
144
+ <div className="mobile-empty">
145
+ <Clock size={40} />
146
+ <p>No schedules found.</p>
147
+ </div>
148
+ ) : (
149
+ <div className="mobile-card-list">
150
+ {filtered.map((s) => (
151
+ <button
152
+ key={s.id}
153
+ type="button"
154
+ className="mobile-list-item mobile-list-item-interactive"
155
+ onClick={() => setDetailSchedule(s)}
156
+ >
157
+ <div className="mobile-list-icon"><Clock size={16} /></div>
158
+ <div className="mobile-list-content">
159
+ <span className="mobile-list-title">{s.name}</span>
160
+ <span className="mobile-list-meta">
161
+ {humanizeSchedule(s)} ·{' '}
162
+ {s.nextRun ? `next ${formatNextShort(s.nextRun, s.timezone)}` : 'no next run'}
163
+ </span>
164
+ </div>
165
+ <span className={`mobile-list-badge ${s.enabled ? 'badge-on' : 'badge-off'}`}>
166
+ {s.enabled ? 'on' : 'off'}
167
+ </span>
168
+ </button>
169
+ ))}
170
+ </div>
171
+ )}
172
+
173
+ {detailSchedule && (
174
+ <MobileBottomSheet
175
+ open={true}
176
+ onClose={() => setDetailSchedule(null)}
177
+ title={detailSchedule.name}
178
+ actions={
179
+ <div className="mobile-task-detail-actions">
180
+ <button
181
+ type="button"
182
+ className="mobile-btn"
183
+ onClick={() => { if (detailSchedule) triggerSchedule(detailSchedule.id); }}
184
+ >
185
+ <Play size={14} /> Trigger now
186
+ </button>
187
+ <button
188
+ type="button"
189
+ className={`mobile-btn ${!detailSchedule.enabled ? '' : 'mobile-btn-secondary'}`}
190
+ onClick={() => { if (detailSchedule) toggleSchedule(detailSchedule.id, !detailSchedule.enabled); }}
191
+ >
192
+ {detailSchedule.enabled ? 'Disable' : 'Enable'}
193
+ </button>
194
+ <button
195
+ type="button"
196
+ className="mobile-btn"
197
+ onClick={() => {
198
+ if (!detailSchedule) return;
199
+ setEditor({ open: true, initial: detailSchedule });
200
+ setDetailSchedule(null);
201
+ }}
202
+ >
203
+ <Pencil size={14} /> Edit
204
+ </button>
205
+ <button
206
+ type="button"
207
+ className="mobile-btn mobile-btn-danger"
208
+ onClick={() => { if (detailSchedule) deleteSchedule(detailSchedule.id); }}
209
+ >
210
+ <Trash2 size={14} />
211
+ </button>
212
+ </div>
213
+ }
214
+ >
215
+ <div className="mobile-agent-detail">
216
+ <div className="mobile-agent-detail-meta">
217
+ <div className="mobile-task-detail-row">
218
+ <span>Type</span>
219
+ <span>{detailSchedule.type}</span>
220
+ </div>
221
+ <div className="mobile-task-detail-row">
222
+ <span>Schedule</span>
223
+ <span className="mono">{humanizeSchedule(detailSchedule) || detailSchedule.schedule}</span>
224
+ </div>
225
+ {detailSchedule.timezone && (
226
+ <div className="mobile-task-detail-row">
227
+ <span>Timezone</span>
228
+ <span className="mono">{detailSchedule.timezone}</span>
229
+ </div>
230
+ )}
231
+ <div className="mobile-task-detail-row">
232
+ <span>Action</span>
233
+ <span>{detailSchedule.action.type}: {detailSchedule.action.target}</span>
234
+ </div>
235
+ {detailSchedule.action.type === 'agent' && detailSchedule.action.prompt && (
236
+ <div className="mobile-task-detail-row">
237
+ <span>Prompt</span>
238
+ <span style={{ fontSize: 12 }}>{detailSchedule.action.prompt}</span>
239
+ </div>
240
+ )}
241
+ {detailSchedule.budgetCheck?.skipIfBudgetLow && (
242
+ <div className="mobile-task-detail-row">
243
+ <span>Budget gate</span>
244
+ <span>skip if ≥ {detailSchedule.budgetCheck.maxConcurrent ?? 6} concurrent</span>
245
+ </div>
246
+ )}
247
+ <div className="mobile-task-detail-row">
248
+ <span>Next run</span>
249
+ <span>{detailSchedule.nextRun ? formatNextShort(detailSchedule.nextRun, detailSchedule.timezone) : '—'}</span>
250
+ </div>
251
+ <div className="mobile-task-detail-row">
252
+ <span>Last run</span>
253
+ <span>{detailSchedule.lastRun ? formatRel(detailSchedule.lastRun) : '—'}</span>
254
+ </div>
255
+ {detailSchedule.lastResult && (
256
+ <div className="mobile-task-detail-row">
257
+ <span>Last result</span>
258
+ <span style={{ fontSize: 12 }}>{detailSchedule.lastResult}</span>
259
+ </div>
260
+ )}
261
+ {detailSchedule.lastError && (
262
+ <div className="mobile-task-detail-row">
263
+ <span>Last error</span>
264
+ <span style={{ fontSize: 12 }}>{detailSchedule.lastError}</span>
265
+ </div>
266
+ )}
267
+ </div>
268
+ </div>
269
+ </MobileBottomSheet>
270
+ )}
271
+
272
+ <ScheduleEditor
273
+ open={editor.open}
274
+ initial={editor.initial}
275
+ onClose={() => setEditor({ open: false })}
276
+ onSaved={() => {
277
+ setEditor({ open: false });
278
+ reload();
279
+ }}
280
+ />
281
+ </div>
282
+ );
283
+ }
284
+
285
+ /* ------------------------------------------------------------------ */
286
+ /* Reusable mobile editor modal */
287
+ /* ------------------------------------------------------------------ */
288
+
289
+ type EditorState = {
290
+ name: string;
291
+ type: Schedule['type'];
292
+ cronMinute: number;
293
+ cronHour: number;
294
+ cronDow: string;
295
+ intervalN: number;
296
+ intervalUnit: string;
297
+ onceAt: string;
298
+ timezone: string;
299
+ actionType: Schedule['action']['type'];
300
+ actionTarget: string;
301
+ actionPrompt: string;
302
+ skipIfBudgetLow: boolean;
303
+ maxConcurrent: number;
304
+ enabled: boolean;
305
+ };
306
+
307
+ function initialEditorState(initial?: Schedule): EditorState {
308
+ if (!initial) {
309
+ return {
310
+ name: '',
311
+ type: 'cron',
312
+ cronMinute: 0,
313
+ cronHour: 13,
314
+ cronDow: '0',
315
+ intervalN: 30,
316
+ intervalUnit: 'm',
317
+ onceAt: '',
318
+ timezone: 'UTC',
319
+ actionType: 'agent',
320
+ actionTarget: '',
321
+ actionPrompt: '',
322
+ skipIfBudgetLow: false,
323
+ maxConcurrent: 6,
324
+ enabled: true,
325
+ };
326
+ }
327
+ const parsedCron = initial.type === 'cron' ? parseSimpleCron(initial.schedule) : null;
328
+ const parsedInterval = initial.type === 'interval' ? parseSimpleInterval(initial.schedule) : null;
329
+ return {
330
+ name: initial.name || '',
331
+ type: initial.type,
332
+ cronMinute: parsedCron?.minute ?? 0,
333
+ cronHour: parsedCron?.hour ?? 9,
334
+ cronDow: parsedCron?.dow ?? '*',
335
+ intervalN: parsedInterval?.n ?? 30,
336
+ intervalUnit: parsedInterval?.unit ?? 'm',
337
+ onceAt: initial.type === 'once' ? toDatetimeLocal(initial.schedule) : '',
338
+ timezone: initial.timezone || 'UTC',
339
+ actionType: initial.action.type,
340
+ actionTarget: initial.action.target || '',
341
+ actionPrompt: initial.action.prompt || '',
342
+ skipIfBudgetLow: !!initial.budgetCheck?.skipIfBudgetLow,
343
+ maxConcurrent: Number.isFinite(initial.budgetCheck?.maxConcurrent)
344
+ ? initial.budgetCheck!.maxConcurrent!
345
+ : 6,
346
+ enabled: initial.enabled !== false,
347
+ };
348
+ }
349
+
350
+ function composeScheduleValue(s: EditorState): string {
351
+ if (s.type === 'cron') return `${s.cronMinute} ${s.cronHour} * * ${s.cronDow}`;
352
+ if (s.type === 'interval') return `${s.intervalN}${s.intervalUnit}`;
353
+ if (!s.onceAt) return '';
354
+ try {
355
+ return new Date(s.onceAt).toISOString();
356
+ } catch {
357
+ return s.onceAt;
358
+ }
359
+ }
360
+
361
+ function ScheduleEditor({
362
+ open,
363
+ initial,
364
+ onClose,
365
+ onSaved,
366
+ }: {
367
+ open: boolean;
368
+ initial?: Schedule;
369
+ onClose: () => void;
370
+ onSaved: () => void;
371
+ }) {
372
+ const [state, setState] = useState<EditorState>(() => initialEditorState(initial));
373
+ const [submitting, setSubmitting] = useState(false);
374
+
375
+ // Re-seed state when the modal is reopened with a different initial.
376
+ useEffect(() => {
377
+ if (open) setState(initialEditorState(initial));
378
+ }, [open, initial]);
379
+
380
+ const update = <K extends keyof EditorState>(key: K, value: EditorState[K]) => {
381
+ setState((cur) => ({ ...cur, [key]: value }));
382
+ };
383
+
384
+ const handleSubmit = async (e: React.FormEvent) => {
385
+ e.preventDefault();
386
+ if (!state.name.trim()) return;
387
+ const schedule = composeScheduleValue(state);
388
+ if (!schedule) return;
389
+ setSubmitting(true);
390
+ try {
391
+ const payload = {
392
+ name: state.name.trim(),
393
+ type: state.type,
394
+ schedule,
395
+ timezone: state.timezone,
396
+ action: {
397
+ type: state.actionType,
398
+ target: state.actionTarget.trim(),
399
+ ...(state.actionType === 'agent' && state.actionPrompt.trim()
400
+ ? { prompt: state.actionPrompt.trim() }
401
+ : {}),
402
+ },
403
+ budgetCheck: {
404
+ maxConcurrent: state.maxConcurrent,
405
+ skipIfBudgetLow: state.skipIfBudgetLow,
406
+ },
407
+ enabled: state.enabled,
408
+ };
409
+ if (initial) {
410
+ await api.put(`/schedules/${encodeURIComponent(initial.id)}`, payload);
411
+ } else {
412
+ await api.post('/schedules', payload);
413
+ }
414
+ onSaved();
415
+ } catch {
416
+ // best-effort — modal stays open so the operator can retry.
417
+ } finally {
418
+ setSubmitting(false);
419
+ }
420
+ };
421
+
422
+ return (
423
+ <MobileModal
424
+ open={open}
425
+ onClose={onClose}
426
+ title={initial ? `Edit ${initial.name}` : 'New schedule'}
427
+ actions={
428
+ <button
429
+ type="submit"
430
+ form="mobile-schedule-form"
431
+ className="mobile-btn"
432
+ style={{ width: '100%' }}
433
+ disabled={submitting}
434
+ >
435
+ <Plus size={14} /> {initial ? 'Save' : 'Create'}
436
+ </button>
437
+ }
438
+ >
439
+ <form id="mobile-schedule-form" onSubmit={handleSubmit} className="mobile-task-form">
440
+ <label className="mobile-field-label">Name *</label>
441
+ <input
442
+ className="mobile-input"
443
+ type="text"
444
+ placeholder="Weekly review"
445
+ value={state.name}
446
+ onChange={(e) => update('name', e.target.value)}
447
+ autoFocus
448
+ required
449
+ />
450
+
451
+ <label className="mobile-field-label">Type</label>
452
+ <select
453
+ className="mobile-input"
454
+ value={state.type}
455
+ onChange={(e) => update('type', e.target.value as Schedule['type'])}
456
+ >
457
+ {TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
458
+ </select>
459
+
460
+ {state.type === 'cron' && (
461
+ <>
462
+ <label className="mobile-field-label">Day of week</label>
463
+ <select
464
+ className="mobile-input"
465
+ value={state.cronDow}
466
+ onChange={(e) => update('cronDow', e.target.value)}
467
+ >
468
+ {DOWS.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
469
+ </select>
470
+
471
+ <div className="mobile-form-row">
472
+ <div style={{ flex: 1 }}>
473
+ <label className="mobile-field-label">Hour</label>
474
+ <select
475
+ className="mobile-input"
476
+ value={String(state.cronHour)}
477
+ onChange={(e) => update('cronHour', parseInt(e.target.value, 10))}
478
+ >
479
+ {HOURS.map((h) => <option key={h.value} value={String(h.value)}>{h.label}</option>)}
480
+ </select>
481
+ </div>
482
+ <div style={{ flex: 1 }}>
483
+ <label className="mobile-field-label">Minute</label>
484
+ <select
485
+ className="mobile-input"
486
+ value={String(state.cronMinute)}
487
+ onChange={(e) => update('cronMinute', parseInt(e.target.value, 10))}
488
+ >
489
+ {MINUTES.map((m) => <option key={m} value={String(m)}>{String(m).padStart(2, '0')}</option>)}
490
+ </select>
491
+ </div>
492
+ </div>
493
+ </>
494
+ )}
495
+
496
+ {state.type === 'interval' && (
497
+ <div className="mobile-form-row">
498
+ <div style={{ flex: 1 }}>
499
+ <label className="mobile-field-label">Every</label>
500
+ <input
501
+ className="mobile-input"
502
+ type="number"
503
+ min={1}
504
+ value={state.intervalN}
505
+ onChange={(e) => update('intervalN', parseInt(e.target.value, 10) || 1)}
506
+ />
507
+ </div>
508
+ <div style={{ flex: 1 }}>
509
+ <label className="mobile-field-label">Unit</label>
510
+ <select
511
+ className="mobile-input"
512
+ value={state.intervalUnit}
513
+ onChange={(e) => update('intervalUnit', e.target.value)}
514
+ >
515
+ {INTERVAL_UNITS.map((u) => <option key={u.value} value={u.value}>{u.label}</option>)}
516
+ </select>
517
+ </div>
518
+ </div>
519
+ )}
520
+
521
+ {state.type === 'once' && (
522
+ <>
523
+ <label className="mobile-field-label">Run at</label>
524
+ <input
525
+ className="mobile-input"
526
+ type="datetime-local"
527
+ value={state.onceAt}
528
+ onChange={(e) => update('onceAt', e.target.value)}
529
+ />
530
+ </>
531
+ )}
532
+
533
+ <label className="mobile-field-label">Timezone</label>
534
+ <select
535
+ className="mobile-input"
536
+ value={TIMEZONES.includes(state.timezone) ? state.timezone : 'UTC'}
537
+ onChange={(e) => update('timezone', e.target.value)}
538
+ >
539
+ {TIMEZONES.map((tz) => <option key={tz} value={tz}>{tz}</option>)}
540
+ </select>
541
+
542
+ <label className="mobile-field-label">Action type</label>
543
+ <select
544
+ className="mobile-input"
545
+ value={state.actionType}
546
+ onChange={(e) => update('actionType', e.target.value as Schedule['action']['type'])}
547
+ >
548
+ {ACTIONS.map((t) => <option key={t} value={t}>{t}</option>)}
549
+ </select>
550
+
551
+ <label className="mobile-field-label">Target</label>
552
+ <input
553
+ className="mobile-input"
554
+ type="text"
555
+ placeholder={
556
+ state.actionType === 'webhook'
557
+ ? 'https://...'
558
+ : state.actionType === 'agent'
559
+ ? 'agent or task ref'
560
+ : 'echo hello'
561
+ }
562
+ value={state.actionTarget}
563
+ onChange={(e) => update('actionTarget', e.target.value)}
564
+ />
565
+
566
+ {state.actionType === 'agent' && (
567
+ <>
568
+ <label className="mobile-field-label">Prompt</label>
569
+ <textarea
570
+ className="mobile-input"
571
+ rows={3}
572
+ placeholder="What should the agent do?"
573
+ value={state.actionPrompt}
574
+ onChange={(e) => update('actionPrompt', e.target.value)}
575
+ />
576
+ </>
577
+ )}
578
+
579
+ <fieldset className="mobile-budget-card">
580
+ <legend>Budget pre-flight</legend>
581
+ <label className="mobile-checkbox-row">
582
+ <input
583
+ type="checkbox"
584
+ checked={state.skipIfBudgetLow}
585
+ onChange={(e) => update('skipIfBudgetLow', e.target.checked)}
586
+ />
587
+ <span>Skip if too many bg tasks are running</span>
588
+ </label>
589
+ <label className="mobile-field-label">Cap</label>
590
+ <input
591
+ className="mobile-input"
592
+ type="number"
593
+ min={1}
594
+ max={64}
595
+ value={state.maxConcurrent}
596
+ onChange={(e) => update('maxConcurrent', parseInt(e.target.value, 10) || 6)}
597
+ disabled={!state.skipIfBudgetLow}
598
+ />
599
+ </fieldset>
600
+
601
+ <label className="mobile-checkbox-row">
602
+ <input
603
+ type="checkbox"
604
+ checked={state.enabled}
605
+ onChange={(e) => update('enabled', e.target.checked)}
606
+ />
607
+ <span>Enabled</span>
608
+ </label>
609
+ </form>
610
+ </MobileModal>
611
+ );
612
+ }
613
+
614
+ /* ------------------------------------------------------------------ */
615
+ /* Helpers */
616
+ /* ------------------------------------------------------------------ */
617
+
618
+ function formatRel(ts: string): string {
619
+ const diff = Date.now() - new Date(ts).getTime();
620
+ if (diff < 60000) return 'just now';
621
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
622
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
623
+ return new Date(ts).toLocaleDateString();
624
+ }
625
+
626
+ function formatNextShort(iso: string, tz?: string | null): string {
627
+ try {
628
+ return new Date(iso).toLocaleString(undefined, {
629
+ timeZone: tz || undefined,
630
+ weekday: 'short',
631
+ hour: 'numeric',
632
+ minute: '2-digit',
633
+ month: 'short',
634
+ day: 'numeric',
635
+ });
636
+ } catch {
637
+ return new Date(iso).toLocaleString();
638
+ }
639
+ }
640
+
641
+ function parseSimpleCron(expr: string): { minute: number; hour: number; dow: string } | null {
642
+ if (typeof expr !== 'string') return null;
643
+ const parts = expr.trim().split(/\s+/);
644
+ if (parts.length !== 5) return null;
645
+ const [m, h, , , dow] = parts;
646
+ const minNum = parseInt(m, 10);
647
+ const hourNum = parseInt(h, 10);
648
+ if (!Number.isFinite(minNum) || !Number.isFinite(hourNum)) return null;
649
+ if (m !== String(minNum) || h !== String(hourNum)) return null;
650
+ return {
651
+ minute: minNum,
652
+ hour: hourNum,
653
+ dow: dow === '*' ? '*' : (Number.isFinite(parseInt(dow, 10)) ? dow : '*'),
654
+ };
655
+ }
656
+
657
+ function parseSimpleInterval(s: string): { n: number; unit: string } | null {
658
+ if (typeof s !== 'string') return null;
659
+ const m = /^(\d+)([smhd])$/i.exec(s.trim());
660
+ if (!m) return null;
661
+ return { n: parseInt(m[1], 10), unit: m[2].toLowerCase() };
662
+ }
663
+
664
+ function toDatetimeLocal(iso: string): string {
665
+ if (!iso) return '';
666
+ try {
667
+ const d = new Date(iso);
668
+ if (Number.isNaN(d.getTime())) return '';
669
+ const pad = (n: number) => String(n).padStart(2, '0');
670
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
671
+ } catch {
672
+ return '';
673
+ }
674
+ }
675
+
676
+ function humanizeSchedule(s: Schedule): string {
677
+ if (s.type !== 'cron') return s.schedule;
678
+ const parsed = parseSimpleCron(s.schedule);
679
+ if (!parsed) return s.schedule;
680
+ const dow = DOWS.find((d) => d.value === parsed.dow)?.label || parsed.dow;
681
+ const hourLabel = HOURS.find((h) => h.value === parsed.hour)?.label || `${parsed.hour}`;
682
+ const minute = String(parsed.minute).padStart(2, '0');
683
+ if (dow === 'Every day') return `Every day ${hourLabel} :${minute}`;
684
+ return `Every ${dow} ${hourLabel} :${minute}`;
685
+ }