@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,812 @@
1
+ // src/views/Overview.tsx — v3.7.0 activity cards + SSE stream.
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import {
4
+ Bot,
5
+ CheckSquare,
6
+ Folder,
7
+ LayoutDashboard,
8
+ Map,
9
+ MessageSquare,
10
+ RefreshCw,
11
+ PlayCircle,
12
+ ShieldCheck,
13
+ FileText,
14
+ Zap,
15
+ Plus,
16
+ Trash2,
17
+ Power,
18
+ Send,
19
+ Sparkles,
20
+ AlertOctagon,
21
+ AlertTriangle,
22
+ CheckCircle2,
23
+ Activity,
24
+ Puzzle,
25
+ Clock,
26
+ FolderSearch,
27
+ EyeOff,
28
+ Eye,
29
+ X,
30
+ } from 'lucide-react';
31
+ import { Card, CardTitle, CardMeta } from '../components/Card';
32
+ import { Button } from '../components/Button';
33
+ import { EmptyState } from '../components/EmptyState';
34
+ import { Spinner } from '../components/Spinner';
35
+ import { useToast } from '../components/Toast';
36
+ import { useModal } from '../components/Modal';
37
+ import { FileBrowser } from '../components/FileBrowser';
38
+ import { api } from '../lib/api';
39
+ import { formatRelative, formatTime } from '../lib/utils';
40
+ import type { Overview, Settings, Snapshot, ActivityItem, ProjectRecord, Mod, ScanResult, DirectoryListing } from '../lib/types';
41
+ import { cn } from '../lib/utils';
42
+
43
+ type Props = {
44
+ snapshot: Snapshot;
45
+ settings: Settings;
46
+ activeTab: string;
47
+ setActiveTab: (id: string) => void;
48
+ refreshSnapshot: () => Promise<void>;
49
+ };
50
+
51
+ export function Overview({
52
+ snapshot,
53
+ settings,
54
+ setActiveTab,
55
+ refreshSnapshot,
56
+ }: Props) {
57
+ const toast = useToast();
58
+ const modal = useModal();
59
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
60
+ const [overview, setOverview] = useState<Overview | null>(
61
+ snapshot.overview ?? null,
62
+ );
63
+ const [loading, setLoading] = useState(!snapshot.overview);
64
+ const [projects, setProjects] = useState<ProjectRecord[]>(snapshot.projects || []);
65
+ const [activeId, setActiveId] = useState<string | null>(
66
+ snapshot.activeProject?.id || null,
67
+ );
68
+ const [mods, setMods] = useState<Mod[]>(snapshot.mods || []);
69
+ const [submitting, setSubmitting] = useState(false);
70
+ // v3.7.0 — Live activity feed via SSE
71
+ const [activityItems, setActivityItems] = useState<ActivityItem[]>(
72
+ snapshot.overview?.recentActivity ?? [],
73
+ );
74
+ const [activityExpanded, setActivityExpanded] = useState(false);
75
+ // v3.15.0 — Hidden event keys are stored server-side; we just track
76
+ // the set in client state. Hiding does NOT delete — see Settings →
77
+ // Activity Log for the full feed.
78
+ const [hiddenKeys, setHiddenKeys] = useState<Set<string>>(new Set());
79
+
80
+ // v3.15.0 — Load hidden set once on mount
81
+ useEffect(() => {
82
+ let cancelled = false;
83
+ (async () => {
84
+ try {
85
+ const r = await api.get<{ hidden: string[] }>('/activity/hidden');
86
+ if (!cancelled) setHiddenKeys(new Set(r.hidden || []));
87
+ } catch { /* non-fatal */ }
88
+ })();
89
+ return () => { cancelled = true; };
90
+ }, []);
91
+
92
+ // v3.15.0 — Helper: stable key per activity item
93
+ const itemKey = (it: ActivityItem, idx: number): string => {
94
+ const k = `${it.kind || ''}|${it.ts || ''}|${(it.slug as string) || (it.title as string) || ''}|${idx}`;
95
+ // hash to a short key (matches server-side activityKey derivation)
96
+ let h = 0;
97
+ for (let i = 0; i < k.length; i++) h = ((h << 5) - h + k.charCodeAt(i)) | 0;
98
+ return Math.abs(h).toString(16).padStart(8, '0').slice(0, 16);
99
+ };
100
+
101
+ const onHide = async (key: string) => {
102
+ const next = new Set(hiddenKeys);
103
+ next.add(key);
104
+ setHiddenKeys(next);
105
+ try {
106
+ await api.post('/activity/hide', { keys: [key] });
107
+ } catch (err) {
108
+ // rollback on failure
109
+ const rollback = new Set(hiddenKeys);
110
+ setHiddenKeys(rollback);
111
+ toast.error(`Hide failed: ${(err as Error).message}`);
112
+ }
113
+ };
114
+
115
+ const onClearAll = async () => {
116
+ if (!confirm('Hide every recent activity item from the overview? The full log stays in Settings → Activity Log.')) return;
117
+ const all = activityItems.map((it, idx) => itemKey(it, idx));
118
+ const next = new Set(hiddenKeys);
119
+ all.forEach((k) => next.add(k));
120
+ setHiddenKeys(next);
121
+ try {
122
+ await api.post('/activity/hide', { keys: all });
123
+ toast.success(`Hidden ${all.length} item(s). Restore them in Settings → Activity Log.`);
124
+ } catch (err) {
125
+ toast.error(`Clear failed: ${(err as Error).message}`);
126
+ }
127
+ };
128
+
129
+ const onRestoreAll = async () => {
130
+ setHiddenKeys(new Set());
131
+ try {
132
+ await api.del('/activity/hide');
133
+ toast.success('All hidden activity restored to the overview.');
134
+ } catch (err) {
135
+ toast.error(`Restore failed: ${(err as Error).message}`);
136
+ }
137
+ };
138
+
139
+ // v3.7.0 — Sync initial snapshot data
140
+ useEffect(() => {
141
+ if (snapshot.overview) {
142
+ setOverview(snapshot.overview);
143
+ setActivityItems(snapshot.overview.recentActivity ?? []);
144
+ setLoading(false);
145
+ }
146
+ setProjects(snapshot.projects || []);
147
+ setActiveId(snapshot.activeProject?.id || null);
148
+ setMods(snapshot.mods || []);
149
+ }, [snapshot.overview, snapshot.projects, snapshot.activeProject, snapshot.mods]);
150
+
151
+ // v3.7.0 — Subscribe to SSE activity stream
152
+ // v3.6.0 — Append ?token=… for auth. EventSource can't set custom
153
+ // headers, so the server accepts the token via query string too.
154
+ useEffect(() => {
155
+ let es: EventSource;
156
+ try {
157
+ const tok = api.getToken();
158
+ const url = tok
159
+ ? `/api/activity/stream?token=${encodeURIComponent(tok)}`
160
+ : '/api/activity/stream';
161
+ es = new EventSource(url);
162
+ es.addEventListener('snapshot', (e) => {
163
+ try {
164
+ const parsed = JSON.parse((e as MessageEvent).data) as { events: ActivityItem[]; generatedAt?: string };
165
+ setActivityItems(Array.isArray(parsed.events) ? parsed.events.slice(0, 50) : []);
166
+ } catch { /* ignore parse errors */ }
167
+ });
168
+ es.addEventListener('activity', (e) => {
169
+ try {
170
+ const entry = JSON.parse((e as MessageEvent).data) as ActivityItem;
171
+ setActivityItems((cur) => [entry, ...cur].slice(0, 50));
172
+ } catch { /* ignore parse errors */ }
173
+ });
174
+ } catch { /* SSE not available — fallback to snapshot data */ }
175
+ return () => { try { es?.close(); } catch { /* ignore */ } };
176
+ }, []);
177
+
178
+ const onRefresh = async () => {
179
+ toast.info('Refreshing…', 1500);
180
+ await refreshSnapshot();
181
+ try {
182
+ const data = await api.get<{ projects: ProjectRecord[]; active: string | null }>('/projects');
183
+ setProjects(data.projects || []);
184
+ setActiveId(data.active || null);
185
+ } catch { /* ignore */ }
186
+ };
187
+
188
+ const onAddProject = () => {
189
+ modal.open({
190
+ title: 'Add project',
191
+ children: (
192
+ <AddProjectDialog
193
+ settings={settings}
194
+ onAdd={async (path: string, name: string | null) => {
195
+ try {
196
+ const r = await api.post<ProjectRecord>('/projects', { path, name });
197
+ setProjects((cur) => [...cur.filter((p) => p.id !== r.id), r]);
198
+ toast.success('Project added.');
199
+ modal.close();
200
+ } catch (err) {
201
+ toast.error(`Add failed: ${(err as Error).message}`);
202
+ }
203
+ }}
204
+ />
205
+ ),
206
+ footer: (
207
+ <div className="modal-footer-actions">
208
+ <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
209
+ </div>
210
+ ),
211
+ });
212
+ };
213
+
214
+ const onUseCurrentDir = async () => {
215
+ try {
216
+ const data = await api.post<{ projects: ProjectRecord[]; active: string | null }>(
217
+ '/projects/auto-detect',
218
+ );
219
+ setProjects(data.projects || []);
220
+ setActiveId(data.active || null);
221
+ const newActive = data.projects?.find((p) => p.id === data.active);
222
+ toast.success(newActive ? `Active: ${newActive.name}` : 'Projects refreshed.');
223
+ await refreshSnapshot();
224
+ } catch (err) {
225
+ toast.error(`Auto-detect failed: ${(err as Error).message}`);
226
+ }
227
+ };
228
+
229
+ const onActivate = async (id: string) => {
230
+ try {
231
+ await api.post(`/projects/${encodeURIComponent(id)}/activate`);
232
+ setActiveId(id);
233
+ toast.success('Project activated.');
234
+ await refreshSnapshot();
235
+ } catch (err) {
236
+ toast.error(`Activate failed: ${(err as Error).message}`);
237
+ }
238
+ };
239
+
240
+ const onRemove = async (id: string) => {
241
+ if (!confirm(`Remove project "${id}" from the registry?`)) return;
242
+ try {
243
+ await api.del(`/projects/${encodeURIComponent(id)}`);
244
+ setProjects((cur) => cur.filter((p) => p.id !== id));
245
+ if (activeId === id) setActiveId(null);
246
+ toast.success('Project removed.');
247
+ } catch (err) {
248
+ toast.error(`Remove failed: ${(err as Error).message}`);
249
+ }
250
+ };
251
+
252
+ if (loading || !overview) {
253
+ return (
254
+ <div className="view-loading">
255
+ <Spinner size="lg" />
256
+ <p>Loading overview…</p>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ return (
262
+ <div className="view view-overview">
263
+ {/* v3.4.0 — Hero takes the spotlight. No card wrapper. */}
264
+ <div className="overview-hero-noframe">
265
+ <h1>What do you want to do?</h1>
266
+ <p className="overview-hero-subtitle">
267
+ Describe what you want — Odin will split it into tasks, create a plan,
268
+ delegate to background agents, and track progress in real time.
269
+ </p>
270
+ <form
271
+ className="overview-hero-form-noframe"
272
+ onSubmit={async (e) => {
273
+ e.preventDefault();
274
+ const text = (inputRef.current?.value || '').trim();
275
+ if (!text) return;
276
+ setSubmitting(true);
277
+ try {
278
+ const r = await api.post<{ subtasks?: unknown[] }>('/tasks/submit', { title: text });
279
+ toast.success(`Odin split it into ${(r.subtasks?.length || 1)} task(s)`);
280
+ if (inputRef.current) inputRef.current.value = '';
281
+ await refreshSnapshot();
282
+ } catch (err) {
283
+ toast.error(`Failed: ${(err as Error).message}`);
284
+ } finally {
285
+ setSubmitting(false);
286
+ }
287
+ }}
288
+ >
289
+ <textarea
290
+ ref={inputRef}
291
+ className="overview-input-hero"
292
+ placeholder="e.g. Implement user authentication with email + password, including registration, login, password reset, and integration tests. Use Bcrypt, JWT tokens, and the existing API style."
293
+ disabled={submitting}
294
+ />
295
+ <div style={{ display: 'flex', gap: 'var(--space-2)', alignItems: 'center', flexWrap: 'wrap' }}>
296
+ <Button
297
+ type="submit"
298
+ variant="primary"
299
+ size="lg"
300
+ disabled={submitting}
301
+ >
302
+ {submitting ? <Spinner size="sm" /> : <Send size={16} />}
303
+ Submit to Odin
304
+ </Button>
305
+ <span className="muted" style={{ fontSize: 12 }}>
306
+ <Sparkles size={12} style={{ display: 'inline', verticalAlign: -2, color: 'var(--accent)' }} />
307
+ {' '}Odin + 12 specialist agents available
308
+ </span>
309
+ </div>
310
+ </form>
311
+
312
+ <div className="overview-quick-actions-row">
313
+ {['Implement feature', 'Fix bug', 'Refactor', 'Investigate', 'Add tests', 'Document', 'Optimize', 'Deploy'].map((action) => (
314
+ <button
315
+ key={action}
316
+ type="button"
317
+ className="overview-quick-chip"
318
+ onClick={() => {
319
+ if (inputRef.current) {
320
+ inputRef.current.value = action;
321
+ inputRef.current.focus();
322
+ }
323
+ }}
324
+ >
325
+ {action}
326
+ </button>
327
+ ))}
328
+ </div>
329
+ </div>
330
+
331
+ <div className="overview-feed">
332
+ <div className="overview-feed-head">
333
+ <h2>Recent activity</h2>
334
+ <div className="overview-feed-head-actions">
335
+ {hiddenKeys.size > 0 && (
336
+ <Button variant="ghost" size="sm" onClick={onRestoreAll} title="Restore hidden items to this overview">
337
+ <Eye size={12} /> Show {hiddenKeys.size} hidden
338
+ </Button>
339
+ )}
340
+ {activityItems.length > 8 && (
341
+ <Button variant="ghost" size="sm" onClick={() => setActivityExpanded((v) => !v)}>
342
+ {activityExpanded ? 'Show less' : 'Show all'}
343
+ </Button>
344
+ )}
345
+ {activityItems.length > 0 && (
346
+ <Button variant="ghost" size="sm" onClick={onClearAll} title="Hide every item from the overview (full log kept)">
347
+ <EyeOff size={12} /> Hide all
348
+ </Button>
349
+ )}
350
+ </div>
351
+ </div>
352
+ {hiddenKeys.size > 0 && (
353
+ <div className="activity-hidden-banner" role="status">
354
+ <span>
355
+ <EyeOff size={12} style={{ verticalAlign: -2, marginRight: 6 }} />
356
+ {hiddenKeys.size} item{hiddenKeys.size === 1 ? '' : 's'} hidden from the overview.
357
+ </span>
358
+ <Button variant="ghost" size="sm" onClick={onRestoreAll}>
359
+ <Eye size={12} /> Show them again
360
+ </Button>
361
+ </div>
362
+ )}
363
+ {activityItems.length === 0 ? (
364
+ <div className="muted" style={{ padding: '24px 0', fontSize: 13 }}>
365
+ No activity yet. Use the chat above or invoke a Bizar command to start a feed.
366
+ </div>
367
+ ) : (
368
+ <div className={cn('activity-feed-list-wrap', !activityExpanded && 'activity-feed-list-wrap-collapsed')}>
369
+ <div className="activity-feed-list">
370
+ {activityItems.slice(0, 30).map((it, idx) => {
371
+ const k = itemKey(it, idx);
372
+ if (hiddenKeys.has(k)) return null;
373
+ return (
374
+ <ActivityFeedItem
375
+ key={`${it.ts}-${idx}`}
376
+ item={it}
377
+ activityKey={k}
378
+ onNavigate={setActiveTab}
379
+ onHide={onHide}
380
+ />
381
+ );
382
+ })}
383
+ </div>
384
+ {!activityExpanded && activityItems.length > 8 && <div className="activity-feed-fade" aria-hidden="true" />}
385
+ </div>
386
+ )}
387
+ </div>
388
+
389
+ {/* v3.4.0 — Below-the-fold: projects + meta (compact) */}
390
+ <Card className="project-picker">
391
+ <CardTitle>
392
+ <Folder size={14} /> Projects
393
+ <Button variant="ghost" size="sm" style={{ marginLeft: 'auto' }} onClick={onAddProject}>
394
+ <Plus size={12} /> Add
395
+ </Button>
396
+ <Button variant="ghost" size="sm" onClick={onUseCurrentDir} title="Use the server's working directory">
397
+ <Plus size={12} /> Auto-detect
398
+ </Button>
399
+ {settings?.dashboard?.projectsDirectory && (
400
+ <Button
401
+ variant="ghost"
402
+ size="sm"
403
+ title={`Scan ${settings.dashboard.projectsDirectory} for projects`}
404
+ onClick={async () => {
405
+ try {
406
+ const r = await api.post<ScanResult>('/projects/scan');
407
+ if (r.error) {
408
+ toast.error(r.error);
409
+ } else {
410
+ toast.success(`Added ${r.added.length}, skipped ${r.skipped}.`);
411
+ }
412
+ await refreshSnapshot();
413
+ const data = await api.get<{ projects: ProjectRecord[]; active: string | null }>('/projects');
414
+ setProjects(data.projects || []);
415
+ setActiveId(data.active || null);
416
+ } catch (err) {
417
+ toast.error(`Scan failed: ${(err as Error).message}`);
418
+ }
419
+ }}
420
+ >
421
+ <FolderSearch size={12} /> Scan
422
+ </Button>
423
+ )}
424
+ <Button variant="ghost" size="sm" onClick={onRefresh} title="Refresh">
425
+ <RefreshCw size={12} />
426
+ </Button>
427
+ </CardTitle>
428
+ <CardMeta>
429
+ {projects.length} project{projects.length === 1 ? '' : 's'} ·
430
+ {' '}{overview.counts.agents} agents ·
431
+ {' '}{overview.counts.sessions} session{overview.counts.sessions === 1 ? '' : 's'}
432
+ </CardMeta>
433
+ {projects.length === 0 ? (
434
+ <EmptyState
435
+ icon={<Folder size={32} />}
436
+ title="No projects yet"
437
+ message="Add a project to start tracking its tasks, plans, and schedules."
438
+ action={
439
+ <div className="empty-state-actions">
440
+ <Button variant="primary" onClick={onUseCurrentDir}>
441
+ <Plus size={14} /> Use current directory
442
+ </Button>
443
+ <Button variant="secondary" onClick={onAddProject}>
444
+ Add by path…
445
+ </Button>
446
+ </div>
447
+ }
448
+ />
449
+ ) : (
450
+ <div className="project-grid">
451
+ {projects.map((p) => (
452
+ <ProjectCard
453
+ key={p.id}
454
+ project={p}
455
+ active={activeId === p.id}
456
+ onOpen={() => onActivate(p.id)}
457
+ onRemove={() => onRemove(p.id)}
458
+ />
459
+ ))}
460
+ </div>
461
+ )}
462
+ </Card>
463
+
464
+ <div className="overview-cols">
465
+ <Card>
466
+ <CardTitle>Mods</CardTitle>
467
+ <CardMeta>Extensions installed under <code>~/.config/bizar/mods/</code></CardMeta>
468
+ {mods.length === 0 ? (
469
+ <div className="muted">No mods installed.</div>
470
+ ) : (
471
+ <ul className="mod-mini-list">
472
+ {mods.map((m) => (
473
+ <li key={m.id} className="mod-mini">
474
+ <span className="mod-mini-name">{m.name}</span>
475
+ <span className="mod-mini-meta">v{m.version} · {m.type}</span>
476
+ <span className={`mod-mini-pill ${m.enabled ? 'mod-mini-pill-on' : 'mod-mini-pill-off'}`}>
477
+ {m.enabled ? 'on' : 'off'}
478
+ </span>
479
+ </li>
480
+ ))}
481
+ </ul>
482
+ )}
483
+ </Card>
484
+
485
+ <Card>
486
+ <CardTitle>Environment</CardTitle>
487
+ <CardMeta>Runtime + paths</CardMeta>
488
+ <dl className="env-table">
489
+ <dt>Node</dt>
490
+ <dd className="mono">{overview.versions.node}</dd>
491
+ <dt>Platform</dt>
492
+ <dd className="mono">{overview.versions.platform}</dd>
493
+ <dt>Project root</dt>
494
+ <dd className="mono ellipsis" title={overview.versions.projectRoot}>
495
+ {overview.versions.projectRoot}
496
+ </dd>
497
+ <dt>Bizar root</dt>
498
+ <dd className="mono ellipsis" title={overview.versions.bizarRoot}>
499
+ {overview.versions.bizarRoot}
500
+ </dd>
501
+ <dt>Generated</dt>
502
+ <dd className="mono tabular-nums">
503
+ {formatTime(overview.generatedAt)}
504
+ </dd>
505
+ </dl>
506
+ </Card>
507
+ </div>
508
+ </div>
509
+ );
510
+ }
511
+
512
+ // ─── AddProjectDialog ─────────────────────────────────────────────────────────
513
+
514
+ /**
515
+ * Pre-flight approach: before POST /projects we verify the selected path
516
+ * still exists (GET /fs?path=...). This avoids the ugly 404 toast when a
517
+ * directory is deleted between selection and submit.
518
+ */
519
+ function AddProjectDialog({
520
+ settings,
521
+ onAdd,
522
+ }: {
523
+ settings: Settings;
524
+ onAdd: (path: string, name: string | null) => void;
525
+ }) {
526
+ const [path, setPath] = useState(settings?.dashboard?.projectsDirectory ?? '');
527
+ const [name, setName] = useState('');
528
+ const [preflighting, setPreflighting] = useState(false);
529
+ const [preflightError, setPreflightError] = useState<string | null>(null);
530
+
531
+ const handleAdd = async () => {
532
+ if (!path) return;
533
+ setPreflighting(true);
534
+ setPreflightError(null);
535
+ try {
536
+ // Pre-flight: verify the path still exists
537
+ await api.get<DirectoryListing>('/fs?path=' + encodeURIComponent(path));
538
+ // Path is valid — proceed with the POST
539
+ onAdd(path, name || null);
540
+ } catch (err) {
541
+ const apiErr = err as { status?: number; data?: { message?: string } };
542
+ if (apiErr.status === 404) {
543
+ setPreflightError('That folder no longer exists. Pick another.');
544
+ } else {
545
+ setPreflightError(
546
+ apiErr.data?.message ?? (err as Error).message ?? 'Validation failed.',
547
+ );
548
+ }
549
+ } finally {
550
+ setPreflighting(false);
551
+ }
552
+ };
553
+
554
+ return (
555
+ <div>
556
+ <label className="field-label">Folder</label>
557
+ <FileBrowser
558
+ value={path}
559
+ onChange={(p) => {
560
+ setPath(p);
561
+ setPreflightError(null);
562
+ }}
563
+ projectsDirectory={settings?.dashboard?.projectsDirectory}
564
+ height={320}
565
+ />
566
+ {preflightError && (
567
+ <p className="field-help" style={{ color: 'var(--error)', marginTop: 4 }}>
568
+ {preflightError}
569
+ </p>
570
+ )}
571
+ <div style={{ marginTop: 'var(--space-3)' }}>
572
+ <label className="field-label" htmlFor="add-project-name">Name (optional)</label>
573
+ <input
574
+ id="add-project-name"
575
+ className="input"
576
+ type="text"
577
+ placeholder="My App"
578
+ value={name}
579
+ onChange={(e) => setName(e.target.value)}
580
+ />
581
+ <p className="field-help" style={{ marginTop: 4 }}>
582
+ Display name for this project. Defaults to the folder name.
583
+ </p>
584
+ </div>
585
+ <div style={{ marginTop: 'var(--space-3)', display: 'flex', justifyContent: 'flex-end' }}>
586
+ <Button
587
+ variant="primary"
588
+ onClick={handleAdd}
589
+ disabled={!path || preflighting}
590
+ >
591
+ {preflighting ? <span className="btn-spinner" /> : null}
592
+ {preflighting ? 'Checking…' : 'Add'}
593
+ </Button>
594
+ </div>
595
+ </div>
596
+ );
597
+ }
598
+
599
+ function ProjectCard({
600
+ project,
601
+ active,
602
+ onOpen,
603
+ onRemove,
604
+ }: {
605
+ project: ProjectRecord;
606
+ active: boolean;
607
+ onOpen: () => void;
608
+ onRemove: () => void;
609
+ }) {
610
+ const statusColor = {
611
+ active: 'status-on',
612
+ inactive: 'status-neutral',
613
+ error: 'status-error',
614
+ }[project.status] || 'status-neutral';
615
+ return (
616
+ <div className={`project-card ${active ? 'project-card-active' : ''}`}>
617
+ <div className="project-card-head">
618
+ <span className={`project-card-status ${statusColor}`}>
619
+ {project.status}
620
+ </span>
621
+ <div className="project-card-name">{project.name}</div>
622
+ <button
623
+ type="button"
624
+ className="icon-btn"
625
+ aria-label="Remove project"
626
+ title="Remove"
627
+ onClick={(e) => {
628
+ e.stopPropagation();
629
+ onRemove();
630
+ }}
631
+ >
632
+ <Trash2 size={12} />
633
+ </button>
634
+ </div>
635
+ <div className="project-card-path mono ellipsis" title={project.path}>
636
+ {project.path}
637
+ </div>
638
+ <div className="project-card-meta">
639
+ {project.lastAccessed && (
640
+ <span className="muted">Last opened {formatRelative(project.lastAccessed)}</span>
641
+ )}
642
+ </div>
643
+ <div className="project-card-actions">
644
+ <Button variant={active ? 'ghost' : 'primary'} size="sm" onClick={onOpen}>
645
+ {active ? <><Power size={12} /> Active</> : <>Open</>}
646
+ </Button>
647
+ </div>
648
+ </div>
649
+ );
650
+ }
651
+
652
+ function sentenceCase(s: string): string {
653
+ if (!s) return s;
654
+ return s.charAt(0).toUpperCase() + s.slice(1);
655
+ }
656
+
657
+ function humanizeKind(kind: string): string {
658
+ const normalized = (kind || 'activity').trim().toLowerCase().replace(/[:_]/g, '.');
659
+ const [scopeRaw, actionRaw = 'updated'] = normalized.split('.');
660
+ const scopeMap: Record<string, string> = {
661
+ settings: 'Settings',
662
+ task: 'Task',
663
+ tasks: 'Task',
664
+ agent: 'Agent',
665
+ agents: 'Agent',
666
+ plan: 'Plan',
667
+ plans: 'Plan',
668
+ bg: 'Background job',
669
+ background: 'Background job',
670
+ session: 'Session',
671
+ mod: 'Mod',
672
+ mods: 'Mod',
673
+ provider: 'Provider',
674
+ providers: 'Provider',
675
+ };
676
+ const actionMap: Record<string, string> = {
677
+ create: 'created', created: 'created',
678
+ add: 'added', added: 'added',
679
+ update: 'updated', updated: 'updated',
680
+ delegate: 'delegated', delegated: 'delegated',
681
+ invoke: 'invoked', invoked: 'invoked',
682
+ restart: 'restarted', restarted: 'restarted',
683
+ delete: 'deleted', deleted: 'deleted',
684
+ remove: 'removed', removed: 'removed',
685
+ archive: 'archived', archived: 'archived',
686
+ restore: 'restored', restored: 'restored',
687
+ complete: 'completed', completed: 'completed',
688
+ fail: 'failed', failed: 'failed',
689
+ error: 'errored', stuck: 'marked stuck',
690
+ };
691
+ const scope = scopeMap[scopeRaw] || sentenceCase(scopeRaw.replace(/-/g, ' '));
692
+ const action = actionMap[actionRaw] || actionRaw.replace(/-/g, ' ');
693
+ return `${scope} ${action}`.trim();
694
+ }
695
+
696
+ function firstString(it: ActivityItem, keys: string[]): string {
697
+ for (const key of keys) {
698
+ const value = it[key];
699
+ if (typeof value === 'string' && value.trim()) return value.trim();
700
+ }
701
+ return '';
702
+ }
703
+
704
+ function formatActivitySummary(it: ActivityItem): string {
705
+ const direct = firstString(it, ['message', 'text', 'prompt', 'title', 'name']);
706
+ if (direct) return direct;
707
+ const parts: string[] = [];
708
+ const slug = firstString(it, ['slug']);
709
+ const agent = firstString(it, ['agent', 'author']);
710
+ const status = firstString(it, ['status']);
711
+ if (slug) parts.push(`Plan ${slug}`);
712
+ if (agent) parts.push(agent);
713
+ if (status) parts.push(status);
714
+ return parts.length ? parts.join(' · ') : 'No additional details.';
715
+ }
716
+
717
+ // v3.7.0 — Format a timestamp as a relative string ("2m ago", "1h ago")
718
+ function formatRelativeTime(ts: string): string {
719
+ return formatRelative(ts);
720
+ }
721
+
722
+ // v3.7.0 — Get severity color token from kind or status field
723
+ function activitySeverity(it: ActivityItem): 'error' | 'warning' | 'success' | 'info' {
724
+ const k = ((it.kind as string) || '').toLowerCase();
725
+ const s = ((it.status as string) || '').toLowerCase();
726
+ if (k.includes('error') || s === 'error' || s === 'failure') return 'error';
727
+ if (k.includes('warn') || s === 'blocked') return 'warning';
728
+ if (k.includes('done') || k.includes('success') || s === 'done') return 'success';
729
+ return 'info';
730
+ }
731
+
732
+ // v3.7.0 — Map activity kind to icon
733
+ function activityIcon(kind: string) {
734
+ const k = kind.toLowerCase();
735
+ if (k === 'task') return CheckSquare;
736
+ if (k === 'agent') return Bot;
737
+ if (k === 'plan') return Map;
738
+ if (k.includes('bg') || k.includes('background') || k.includes('job')) return Zap;
739
+ if (k === 'mod') return Puzzle;
740
+ if (k === 'skill') return Sparkles;
741
+ if (k.includes('error') || k.includes('failure')) return AlertOctagon;
742
+ if (k.includes('warn')) return AlertTriangle;
743
+ return Activity;
744
+ }
745
+
746
+ // v3.7.0 — Navigate to the relevant tab when a card is clicked
747
+ function activityNavTarget(kind: string): string | null {
748
+ const k = kind.toLowerCase();
749
+ if (k === 'task') return 'tasks';
750
+ if (k === 'agent') return 'agents';
751
+ if (k === 'plan') return 'artifacts';
752
+ if (k.includes('bg') || k.includes('background')) return 'activity';
753
+ if (k === 'mod') return 'mods';
754
+ if (k === 'skill') return 'skills';
755
+ return null;
756
+ }
757
+
758
+ // v3.7.0 — Activity card component
759
+ function ActivityFeedItem({
760
+ item,
761
+ activityKey,
762
+ onNavigate,
763
+ onHide,
764
+ }: {
765
+ item: ActivityItem;
766
+ activityKey: string;
767
+ onNavigate: (tab: string) => void;
768
+ onHide: (key: string) => void;
769
+ }) {
770
+ const severity = activitySeverity(item);
771
+ const Icon = activityIcon(item.kind || '');
772
+ const title = humanizeKind(item.kind || 'activity');
773
+ const msg = formatActivitySummary(item);
774
+ const navTarget = activityNavTarget(item.kind || '');
775
+
776
+ const accentColor =
777
+ severity === 'error' ? 'var(--error)' :
778
+ severity === 'warning' ? 'var(--warning)' :
779
+ severity === 'success' ? 'var(--success)' :
780
+ 'var(--accent)';
781
+
782
+ return (
783
+ <div className={cn('activity-feed-row', `activity-feed-row-${severity}`)}>
784
+ <button
785
+ type="button"
786
+ className="activity-feed-row-main"
787
+ onClick={() => { if (navTarget) onNavigate(navTarget); }}
788
+ title={navTarget ? `Open ${navTarget}` : title}
789
+ >
790
+ <div className="activity-feed-icon" style={{ color: accentColor }}>
791
+ <Icon size={14} />
792
+ </div>
793
+ <div className="activity-feed-body">
794
+ <div className="activity-feed-title-row">
795
+ <div className="activity-feed-title">{title}</div>
796
+ <div className="activity-feed-time text-xs muted tabular-nums">{formatRelativeTime(item.ts)}</div>
797
+ </div>
798
+ <div className="activity-feed-summary text-sm">{msg}</div>
799
+ </div>
800
+ </button>
801
+ <button
802
+ type="button"
803
+ className="activity-feed-hide-btn"
804
+ onClick={() => onHide(activityKey)}
805
+ title="Hide this from the overview (kept in Settings → Activity Log)"
806
+ aria-label="Hide from overview"
807
+ >
808
+ <X size={12} />
809
+ </button>
810
+ </div>
811
+ );
812
+ }