@polderlabs/bizar 3.23.1 → 4.2.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 (361) hide show
  1. package/README.md +83 -16
  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 +329 -0
  60. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  61. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  62. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  63. package/bizar-dash/src/server/memory-store.mjs +471 -0
  64. package/bizar-dash/src/server/mod-security.mjs +398 -0
  65. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  66. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  67. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  68. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  69. package/bizar-dash/src/server/pair-store.mjs +138 -0
  70. package/bizar-dash/src/server/projects-store.mjs +364 -0
  71. package/bizar-dash/src/server/providers-store.mjs +789 -0
  72. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  73. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  74. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  75. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  76. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  77. package/bizar-dash/src/server/routes/background.mjs +165 -0
  78. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  79. package/bizar-dash/src/server/routes/config.mjs +129 -0
  80. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  81. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  82. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  83. package/bizar-dash/src/server/routes/history.mjs +78 -0
  84. package/bizar-dash/src/server/routes/memory.mjs +658 -0
  85. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  86. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  87. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  88. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  89. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  90. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  91. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  92. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  93. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  94. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  95. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  96. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  97. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  98. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  99. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  100. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  101. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  102. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  103. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  104. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  105. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  106. package/bizar-dash/src/server/search-store.mjs +220 -0
  107. package/bizar-dash/src/server/serve-info.mjs +684 -0
  108. package/bizar-dash/src/server/server.mjs +796 -0
  109. package/bizar-dash/src/server/settings-store.mjs +45 -0
  110. package/bizar-dash/src/server/skills-store.mjs +300 -0
  111. package/bizar-dash/src/server/state.mjs +437 -0
  112. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  113. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  114. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  115. package/bizar-dash/src/server/tui.mjs +923 -0
  116. package/bizar-dash/src/server/update-store.mjs +168 -0
  117. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  118. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  119. package/bizar-dash/src/server/watcher.mjs +81 -0
  120. package/bizar-dash/src/server/yaml.mjs +238 -0
  121. package/bizar-dash/src/web/App.tsx +718 -0
  122. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  123. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  124. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  125. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  126. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  127. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  128. package/bizar-dash/src/web/components/Button.tsx +56 -0
  129. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  130. package/bizar-dash/src/web/components/Card.tsx +40 -0
  131. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  132. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  133. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  134. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  135. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  136. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  137. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  138. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  139. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  140. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  141. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  142. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  143. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  144. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  145. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  146. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  147. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  148. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  149. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  150. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  151. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  152. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  153. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  154. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  155. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  156. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  157. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  158. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  159. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  160. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  161. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  162. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  163. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  164. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  165. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  166. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  167. package/bizar-dash/src/web/index.html +37 -0
  168. package/bizar-dash/src/web/lib/api.ts +193 -0
  169. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  170. package/bizar-dash/src/web/lib/types.ts +668 -0
  171. package/bizar-dash/src/web/lib/utils.ts +114 -0
  172. package/bizar-dash/src/web/lib/ws.ts +176 -0
  173. package/bizar-dash/src/web/main.tsx +34 -0
  174. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  175. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  176. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  177. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  178. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  179. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  180. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  181. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  183. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  184. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  185. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  186. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  188. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  189. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  190. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  194. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  195. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  196. package/bizar-dash/src/web/mobile.html +20 -0
  197. package/bizar-dash/src/web/mobile.tsx +13 -0
  198. package/bizar-dash/src/web/styles/chat.css +198 -0
  199. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  200. package/bizar-dash/src/web/styles/main.css +8855 -0
  201. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  202. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  203. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  204. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  205. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  206. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  207. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  208. package/bizar-dash/src/web/views/Config.tsx +2063 -0
  209. package/bizar-dash/src/web/views/History.tsx +335 -0
  210. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  211. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  212. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  213. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  214. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  215. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  216. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  217. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  218. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  219. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  220. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  221. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  222. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  223. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  224. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  225. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  226. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  227. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  228. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  229. package/bizar-dash/tests/memory-cli.test.mjs +542 -0
  230. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  231. package/bizar-dash/tests/memory-git.test.mjs +246 -0
  232. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  233. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  234. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  235. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  236. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  237. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  238. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  239. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  240. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  241. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  242. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  243. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  244. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  245. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  246. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  247. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  248. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  249. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  250. package/bizar-dash/tests/yaml.test.mjs +128 -0
  251. package/bizar-dash/tsconfig.json +23 -0
  252. package/bizar-dash/vite.config.ts +27 -0
  253. package/cli/atomic.mjs +73 -0
  254. package/cli/banner.mjs +1 -1
  255. package/cli/bin.mjs +44 -47
  256. package/cli/bootstrap.mjs +1 -1
  257. package/cli/copy.mjs +25 -16
  258. package/cli/dev-link.mjs +17 -1
  259. package/cli/doctor.mjs +4 -4
  260. package/cli/doctor.test.mjs +2 -2
  261. package/cli/init.mjs +135 -1
  262. package/cli/install.mjs +31 -39
  263. package/cli/memory-constants.mjs +59 -0
  264. package/cli/memory.mjs +1436 -0
  265. package/cli/service.mjs +5 -6
  266. package/cli/utils.mjs +6 -3
  267. package/config/AGENTS.md +7 -7
  268. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  269. package/config/opencode.json +13 -38
  270. package/config/skills/memory-protocol/SKILL.md +105 -0
  271. package/config/skills/obsidian/SKILL.md +270 -65
  272. package/install.sh +59 -19
  273. package/package.json +49 -14
  274. package/packages/sdk/LICENSE +21 -0
  275. package/packages/sdk/README.md +80 -0
  276. package/packages/sdk/src/client.ts +183 -0
  277. package/packages/sdk/src/errors.ts +120 -0
  278. package/packages/sdk/src/events.ts +153 -0
  279. package/packages/sdk/src/index.ts +63 -0
  280. package/packages/sdk/src/types.ts +176 -0
  281. package/packages/sdk/src/version.ts +4 -0
  282. package/packages/sdk/tests/client.test.ts +217 -0
  283. package/packages/sdk/tests/errors.test.ts +108 -0
  284. package/packages/sdk/tests/events.test.ts +139 -0
  285. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  286. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  287. package/packages/sdk/tsconfig.json +25 -0
  288. package/packages/sdk/vitest.config.ts +9 -0
  289. package/plugins/bizar/LICENSE +21 -0
  290. package/plugins/bizar/README.md +448 -0
  291. package/plugins/bizar/dist/index.js +29901 -0
  292. package/plugins/bizar/index.ts +1491 -0
  293. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  294. package/plugins/bizar/src/background-state.ts +551 -0
  295. package/plugins/bizar/src/background.ts +1250 -0
  296. package/plugins/bizar/src/commands-impl.ts +464 -0
  297. package/plugins/bizar/src/commands.ts +1168 -0
  298. package/plugins/bizar/src/dashboard-client.ts +233 -0
  299. package/plugins/bizar/src/event-stream.ts +606 -0
  300. package/plugins/bizar/src/fingerprint.ts +120 -0
  301. package/plugins/bizar/src/handoff.ts +79 -0
  302. package/plugins/bizar/src/http-client.ts +467 -0
  303. package/plugins/bizar/src/key-rotation.ts +218 -0
  304. package/plugins/bizar/src/logger.ts +144 -0
  305. package/plugins/bizar/src/loop.ts +176 -0
  306. package/plugins/bizar/src/opencode-runner.ts +390 -0
  307. package/plugins/bizar/src/options.ts +421 -0
  308. package/plugins/bizar/src/plan-fs.ts +323 -0
  309. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  310. package/plugins/bizar/src/report.ts +178 -0
  311. package/plugins/bizar/src/research-prompt.ts +35 -0
  312. package/plugins/bizar/src/serve-info.ts +228 -0
  313. package/plugins/bizar/src/serve.ts +496 -0
  314. package/plugins/bizar/src/settings.ts +349 -0
  315. package/plugins/bizar/src/state.ts +298 -0
  316. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  317. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  318. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  319. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  320. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  321. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  322. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  323. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  324. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  325. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  326. package/plugins/bizar/tests/background-state.test.ts +277 -0
  327. package/plugins/bizar/tests/background.test.ts +402 -0
  328. package/plugins/bizar/tests/block.test.ts +195 -0
  329. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  330. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  331. package/plugins/bizar/tests/commands.test.ts +584 -0
  332. package/plugins/bizar/tests/config.test.ts +122 -0
  333. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  334. package/plugins/bizar/tests/dispose.test.ts +336 -0
  335. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  336. package/plugins/bizar/tests/event.test.ts +262 -0
  337. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  338. package/plugins/bizar/tests/http-client.test.ts +404 -0
  339. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  340. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  341. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  342. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  343. package/plugins/bizar/tests/loop.test.ts +397 -0
  344. package/plugins/bizar/tests/options.test.ts +276 -0
  345. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  346. package/plugins/bizar/tests/serve.test.ts +339 -0
  347. package/plugins/bizar/tests/settings.test.ts +351 -0
  348. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  349. package/plugins/bizar/tests/state.test.ts +276 -0
  350. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  351. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  352. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  353. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  354. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  355. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  356. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  357. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  358. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  359. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  360. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  361. package/plugins/bizar/tsconfig.json +29 -0
@@ -0,0 +1,718 @@
1
+ // src/App.tsx — root shell. Wires data + contexts + tab routing.
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Topbar, TABS } from './components/Topbar';
5
+ import { Sidebar } from './components/Sidebar';
6
+ import { ModalProvider, useModal } from './components/Modal';
7
+ import { ToastProvider, useToast } from './components/Toast';
8
+ import { SearchModal } from './components/SearchModal';
9
+ import { Notifications } from './components/Notifications';
10
+ import { CommandDialog, type DialogDescriptor } from './components/CommandDialog';
11
+ import { api } from './lib/api';
12
+ import { Ws } from './lib/ws';
13
+ import {
14
+ applyTheme,
15
+ applyThemeTokens,
16
+ type Artifact,
17
+ type Settings,
18
+ type SettingsResponse,
19
+ type Snapshot,
20
+ type WsMessage,
21
+ type WsStatus,
22
+ type ProjectRecord,
23
+ type SearchResult,
24
+ } from './lib/types';
25
+ import { Overview } from './views/Overview';
26
+ import { Chat } from './views/Chat';
27
+ import { Agents } from './views/Agents';
28
+ import { Artifacts } from './views/Artifacts';
29
+ import { Tasks } from './views/Tasks';
30
+ import { Activity } from './views/Activity';
31
+ import { Config } from './views/Config';
32
+ import { SettingsView } from './views/Settings';
33
+ import { Mods } from './views/Mods';
34
+ import { ModView, type ModView as ModViewType } from './views/ModView';
35
+ import { Schedules } from './views/Schedules';
36
+ import { Skills } from './views/Skills';
37
+ import { History } from './views/History';
38
+ import { BackgroundAgents } from './views/BackgroundAgents';
39
+ import { Spinner } from './components/Spinner';
40
+ import { Button } from './components/Button';
41
+ import { AlertTriangle, Globe, LayoutTemplate, X } from 'lucide-react';
42
+ import './styles/main.css';
43
+
44
+ type ViewProps = {
45
+ snapshot: Snapshot;
46
+ settings: Settings;
47
+ activeTab: string;
48
+ setActiveTab: (id: string) => void;
49
+ refreshSnapshot: () => Promise<void>;
50
+ /**
51
+ * v3.3.0 — Cross-view scratch state. The "Open in chat" button
52
+ * on the Agents tab sets `pendingInitialAgent`, which the Chat
53
+ * view reads on mount (and clears). Keeps the chat a true
54
+ * one-click handoff without needing URL state.
55
+ */
56
+ crossState?: CrossViewState;
57
+ setCrossState?: (patch: Partial<CrossViewState>) => void;
58
+ };
59
+
60
+ export type CrossViewState = {
61
+ initialAgent?: string | null;
62
+ };
63
+
64
+ const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
65
+ overview: Overview,
66
+ chat: Chat,
67
+ agents: Agents,
68
+ artifacts: Artifacts,
69
+ tasks: Tasks,
70
+ activity: Activity,
71
+ background: BackgroundAgents,
72
+ config: Config,
73
+ settings: SettingsView,
74
+ mods: Mods,
75
+ schedules: Schedules,
76
+ skills: Skills,
77
+ history: History,
78
+ };
79
+
80
+ const VERSION = 'v3.21.0';
81
+
82
+ /**
83
+ * Render the active view. If `activeTab` matches a built-in tab id,
84
+ * use VIEW_MAP. If it matches a mod view id (any id not in VIEW_MAP),
85
+ * render the ModView component with that view id.
86
+ */
87
+ function renderActiveView(
88
+ activeTab: string,
89
+ viewProps: ViewProps,
90
+ modViews: ModViewType[],
91
+ reloadKey: number,
92
+ ): React.ReactNode {
93
+ if (VIEW_MAP[activeTab]) {
94
+ const V = VIEW_MAP[activeTab];
95
+ return <V {...viewProps} />;
96
+ }
97
+ // Mod view — find by id
98
+ const mv = modViews.find((v) => v.id === activeTab);
99
+ if (mv) {
100
+ return (
101
+ <ModView
102
+ viewId={mv.id}
103
+ reloadKey={reloadKey}
104
+ activeTab={viewProps.activeTab}
105
+ setActiveTab={viewProps.setActiveTab}
106
+ />
107
+ );
108
+ }
109
+ // Fallback to Overview
110
+ const V = VIEW_MAP.overview;
111
+ return <V {...viewProps} />;
112
+ }
113
+
114
+ export function App() {
115
+ return (
116
+ <ToastProvider>
117
+ <ModalProvider>
118
+ <Shell />
119
+ </ModalProvider>
120
+ </ToastProvider>
121
+ );
122
+ }
123
+
124
+ function Shell() {
125
+ const toast = useToast();
126
+ const modalApi = useModal();
127
+ const { isModalOpen } = modalApi;
128
+ const isModalOpenRef = useRef(false);
129
+ const [activeTab, setActiveTab] = useState<string>('overview');
130
+ const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
131
+ const [settings, setSettings] = useState<Settings | null>(null);
132
+ const [wsStatus, setWsStatus] = useState<WsStatus>('connecting');
133
+ const [bootError, setBootError] = useState<string | null>(null);
134
+ const [searchOpen, setSearchOpen] = useState(false);
135
+ const [stuckAgents, setStuckAgents] = useState<{ name: string }[]>([]);
136
+ const [stuckBannerDismissed, setStuckBannerDismissed] = useState(false);
137
+ // v3.20.3 — Mod views become first-class nav entries. Fetched on mount
138
+ // and after every mod install/uninstall/enable toggle.
139
+ const [modViews, setModViews] = useState<ModViewType[]>([]);
140
+ const [modViewsReloadKey, setModViewsReloadKey] = useState(0);
141
+ const wsRef = useRef<Ws | null>(null);
142
+ const [ws, setWs] = useState<Ws | null>(null);
143
+
144
+ // Fetch installed mod views (any enabled mod with a web/index.html or
145
+ // a views/registry.json entry). These become top-level nav tabs.
146
+ useEffect(() => {
147
+ let cancelled = false;
148
+ (async () => {
149
+ try {
150
+ const r = await api.get<{ views: ModViewType[] }>('/mods/views');
151
+ if (cancelled) return;
152
+ setModViews(r.views || []);
153
+ } catch {
154
+ if (!cancelled) setModViews([]);
155
+ }
156
+ })();
157
+ return () => { cancelled = true; };
158
+ }, [modViewsReloadKey, snapshot?.mods]);
159
+
160
+ // Re-fetch mod views when ws events arrive about mod changes.
161
+ useEffect(() => {
162
+ if (!ws) return;
163
+ const off = ws.on((msg: { type?: string }) => {
164
+ if (msg?.type === 'mod:change' || msg?.type === 'mod:enabled' || msg?.type === 'mod:installed') {
165
+ setModViewsReloadKey((n) => n + 1);
166
+ }
167
+ });
168
+ return () => off();
169
+ }, [ws]);
170
+
171
+ // Apply theme tokens
172
+ useEffect(() => {
173
+ if (settings?.theme) {
174
+ applyTheme(settings.theme);
175
+ applyThemeTokens(settings.theme);
176
+ }
177
+ }, [settings?.theme]);
178
+
179
+ // v3.6.1 — Keep a ref in sync with isModalOpen so the keyboard handler
180
+ // always reads the current value even between state updates and effect
181
+ // re-runs. This closes the race window where modal.close() has cleared
182
+ // the state but the old keyboard handler (with isModalOpen=false in
183
+ // its closure) hasn't been replaced yet.
184
+ useEffect(() => {
185
+ isModalOpenRef.current = isModalOpen;
186
+ }, [isModalOpen]);
187
+
188
+ // System-mode listener
189
+ useEffect(() => {
190
+ if (settings?.theme?.mode !== 'system') return;
191
+ const mql = window.matchMedia('(prefers-color-scheme: light)');
192
+ const handler = () => applyTheme(settings.theme.mode);
193
+ mql.addEventListener('change', handler);
194
+ return () => mql.removeEventListener('change', handler);
195
+ }, [settings?.theme?.mode]);
196
+
197
+ // Initial fetch — runs once on mount
198
+ useEffect(() => {
199
+ let cancelled = false;
200
+ const loadBootData = () =>
201
+ Promise.all([
202
+ api.get<Snapshot>('/snapshot').catch(() => null),
203
+ api.get<SettingsResponse>('/settings').catch(() => null),
204
+ api.get<{ stuck: { name: string }[] }>('/agents/stuck').catch(() => null),
205
+ ]);
206
+
207
+ const applyBootData = (
208
+ snap: Snapshot | null,
209
+ set: SettingsResponse | null,
210
+ stuck: { stuck: { name: string }[] } | null,
211
+ ) => {
212
+ if (snap) setSnapshot(snap);
213
+ if (set?.data) setSettings(set.data);
214
+ if (stuck?.stuck) setStuckAgents(stuck.stuck);
215
+ };
216
+
217
+ (async () => {
218
+ try {
219
+ const auth = await api.probeAuthStatus();
220
+ const [snap, set, stuck] = await loadBootData();
221
+ if (cancelled) return;
222
+
223
+ applyBootData(snap, set, stuck);
224
+ if (snap || set) return;
225
+
226
+ if (!auth.loopback) {
227
+ setBootError('Dashboard server unreachable.');
228
+ return;
229
+ }
230
+
231
+ await new Promise((resolve) => setTimeout(resolve, 1000));
232
+ if (cancelled) return;
233
+
234
+ const [retrySnap, retrySet, retryStuck] = await loadBootData();
235
+ if (cancelled) return;
236
+
237
+ applyBootData(retrySnap, retrySet, retryStuck);
238
+ if (!retrySnap && !retrySet) {
239
+ setBootError('Dashboard server unreachable.');
240
+ }
241
+ } catch (err) {
242
+ if (cancelled) return;
243
+ const msg = (err as Error)?.message ?? 'unknown error';
244
+ setBootError(msg);
245
+ toast.error(`Failed to load: ${msg}`);
246
+ }
247
+ })();
248
+
249
+ return () => {
250
+ cancelled = true;
251
+ };
252
+ }, [toast]);
253
+
254
+ // Apply defaultTab ONCE on mount only — must NOT re-fire on toast changes
255
+ useEffect(() => {
256
+ let cancelled = false;
257
+ api.get<SettingsResponse>('/settings')
258
+ .then((set) => {
259
+ if (cancelled) return;
260
+ const defaultTab = set?.data?.ui?.defaultTab;
261
+ if (defaultTab) setActiveTab(defaultTab);
262
+ })
263
+ .catch(() => undefined);
264
+ return () => {
265
+ cancelled = true;
266
+ };
267
+ }, []); // empty deps — runs once on mount
268
+
269
+ // v3.1.0 — Periodic stuck-agent poll. Light-weight; the server does
270
+ // the threshold math and only returns the list.
271
+ useEffect(() => {
272
+ let cancelled = false;
273
+ const tick = async () => {
274
+ try {
275
+ const r = await api.get<{ stuck: { name: string }[] }>('/agents/stuck');
276
+ if (!cancelled) {
277
+ setStuckAgents((cur) => {
278
+ const next = r.stuck || [];
279
+ const wasEmpty = cur.length === 0;
280
+ if (wasEmpty && next.length > 0) {
281
+ toast.warning(`${next.length} agent${next.length === 1 ? '' : 's'} stuck`, 5000);
282
+ }
283
+ return next;
284
+ });
285
+ }
286
+ } catch {
287
+ /* best-effort */
288
+ }
289
+ };
290
+ const id = setInterval(tick, 30_000);
291
+ tick();
292
+ return () => {
293
+ cancelled = true;
294
+ clearInterval(id);
295
+ };
296
+ // eslint-disable-next-line react-hooks/exhaustive-deps
297
+ }, []);
298
+
299
+ // WebSocket lifecycle
300
+ useEffect(() => {
301
+ const ws = new Ws();
302
+ wsRef.current = ws;
303
+ setWs(ws);
304
+ const offStatus = ws.onStatus((s) => setWsStatus(s));
305
+ const offMsg = ws.on((msg: WsMessage) => {
306
+ if (msg.type === 'snapshot' && 'data' in msg && msg.data) {
307
+ setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...msg.data }));
308
+ } else if (msg.type === 'change') {
309
+ const m = msg;
310
+ const file = m.path?.split('/').pop() || m.path || '';
311
+ toast.info(`File changed: ${file}`, 2500);
312
+ api
313
+ .get<Snapshot>('/snapshot')
314
+ .then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
315
+ .catch(() => undefined);
316
+ } else if (msg.type === 'tasks:change') {
317
+ const m = msg;
318
+ setSnapshot((cur) => {
319
+ if (!cur) return cur;
320
+ const tasks = (cur.tasks || []).map((t) =>
321
+ t.id === m.task.id ? m.task : t,
322
+ );
323
+ const exists = tasks.some((t) => t.id === m.task.id);
324
+ return { ...cur, tasks: exists ? tasks : [m.task, ...tasks] };
325
+ });
326
+ } else if (msg.type === 'tasks:delete') {
327
+ const m = msg;
328
+ setSnapshot((cur) =>
329
+ cur
330
+ ? { ...cur, tasks: (cur.tasks || []).filter((t) => t.id !== m.id) }
331
+ : cur,
332
+ );
333
+ } else if (msg.type === 'settings:change') {
334
+ const m = msg;
335
+ if (m.settings) setSettings(m.settings);
336
+ } else if (msg.type === 'project:change') {
337
+ // Refresh the whole snapshot to pick up the new active project
338
+ api
339
+ .get<Snapshot>('/snapshot')
340
+ .then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
341
+ .catch(() => undefined);
342
+ } else if (msg.type === 'agents:change' || msg.type === 'schedules:change') {
343
+ api
344
+ .get<Snapshot>('/snapshot')
345
+ .then((s) => setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })))
346
+ .catch(() => undefined);
347
+ } else if (msg.type === 'agent:status' || msg.type === 'agent:restarted') {
348
+ const m = msg;
349
+ setSnapshot((cur) => {
350
+ if (!cur) return cur;
351
+ const agents = (cur.agents || []).map((a) => (a.name === m.agent.name ? m.agent : a));
352
+ return { ...cur, agents };
353
+ });
354
+ } else if (msg.type === 'artifact:change') {
355
+ // Artifacts list refreshes after any artifact mutation.
356
+ api
357
+ .get<{ artifacts: Artifact[] }>('/artifacts')
358
+ .then((d) => {
359
+ setSnapshot((cur) => (cur ? { ...cur, artifacts: d.artifacts || [] } : cur));
360
+ })
361
+ .catch(() => undefined);
362
+ } else if (msg.type === 'agent:stuck') {
363
+ const m = msg;
364
+ setStuckAgents(m.agents || []);
365
+ } else if (msg.type === 'dialog:show') {
366
+ // v0.5.1: Open a command dialog received from the plugin via WS broadcast.
367
+ const m = msg as { type: 'dialog:show'; dialog: DialogDescriptor };
368
+ if (m.dialog) {
369
+ modalApi.open({
370
+ title: m.dialog.title,
371
+ width: 520,
372
+ children: <CommandDialog dialog={m.dialog} onClose={() => modalApi.close()} />,
373
+ });
374
+ }
375
+ }
376
+ });
377
+ return () => {
378
+ offStatus();
379
+ offMsg();
380
+ ws.close();
381
+ setWs(null);
382
+ };
383
+ }, [toast]);
384
+
385
+ const subscribeToWs = useCallback((cb: (msg: WsMessage) => void) => {
386
+ if (!ws) return () => undefined;
387
+ return ws.on(cb);
388
+ }, [ws]);
389
+
390
+ // v3.3.1 — Track recent user interactions. After any mousedown/click/
391
+ // focusin/keydown we set a 1500ms "safe" window during which digit-key
392
+ // shortcuts are suppressed. This protects against the case where a modal
393
+ // closes on a click and the user's next key event (often a key-repeat)
394
+ // would otherwise trigger setActiveTab("overview").
395
+ // v3.3.0 used 250ms which was too short (keyboard repeat can fire later).
396
+ //
397
+ // v3.5.4 (bug #4) — The safe window alone was not enough. Users were
398
+ // still being bounced to overview after action grace windows expired
399
+ // because the transientFocus check (activeElement === body) was nested
400
+ // inside the safe-window branch. See the handler below for the new
401
+ // two-gate model: (1) body/null focus ALWAYS blocks digit shortcuts;
402
+ // (2) the safe window relaxes that block only for the first 1500ms
403
+ // after an interaction. After the window passes, the user must
404
+ // explicitly focus a real element before digit-key tab switches work.
405
+ const safeUntilRef = useRef(0);
406
+ useEffect(() => {
407
+ const bump = () => {
408
+ safeUntilRef.current = Date.now() + 1500;
409
+ };
410
+ document.addEventListener('mousedown', bump, true);
411
+ document.addEventListener('click', bump, true);
412
+ document.addEventListener('focusin', bump, true);
413
+ document.addEventListener('keydown', bump, true);
414
+ return () => {
415
+ document.removeEventListener('mousedown', bump, true);
416
+ document.removeEventListener('click', bump, true);
417
+ document.removeEventListener('focusin', bump, true);
418
+ document.removeEventListener('keydown', bump, true);
419
+ };
420
+ }, []);
421
+
422
+ // Keyboard shortcuts
423
+ useEffect(() => {
424
+ const map: Record<string, string> = {};
425
+ TABS.forEach((t, i) => {
426
+ map[String(i + 1)] = t.id;
427
+ });
428
+ const handler = (e: KeyboardEvent) => {
429
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
430
+ e.preventDefault();
431
+ setSearchOpen(true);
432
+ return;
433
+ }
434
+ if (e.key === '/' && !e.metaKey && !e.ctrlKey) {
435
+ const t = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
436
+ if (t !== 'input' && t !== 'textarea' && !((e.target as HTMLElement)?.isContentEditable)) {
437
+ e.preventDefault();
438
+ setSearchOpen(true);
439
+ return;
440
+ }
441
+ }
442
+ // v3.2.0 — broader guard. Digit-key shortcuts must not fire while
443
+ // the user is interacting with ANY focusable form control or a
444
+ // modal/contentEditable region. Previously this only excluded
445
+ // <input> and <textarea>, which let digit presses through when
446
+ // focus was on a <select>, <button>, <label>, or contenteditable
447
+ // node — causing mysterious tab switches (most often back to
448
+ // "overview" via the `1` shortcut).
449
+ //
450
+ // v3.3.1 — "click after modal close" guard (extended). When a modal
451
+ // closes (e.g. the user clicks "Submit to Odin" in the Tasks tab),
452
+ // React portals the modal out of the DOM and the previously-focused
453
+ // button is unmounted. Focus falls back to <body>. If the user then
454
+ // accidentally taps a digit key (1/2/3/...) — or a key-repeat fires
455
+ // while they're still mid-click — the handler would switch tabs.
456
+ // The safe window is now 1500ms (up from 250ms) and covers all
457
+ // interaction types (mousedown/click/focusin/keydown).
458
+ //
459
+ // v3.3.1 — transient-focus check. If active element is body/null,
460
+ // treat it as a transient focus state (modal close → body fallback)
461
+ // and bail. v3.5.4 (bug #4): this check is now independent of the
462
+ // safe window — it ALWAYS applies. Previously it was only effective
463
+ // inside the 1500ms grace window, which let digit-key presses through
464
+ // once the window expired. The user would see themselves bounced back
465
+ // to overview (digit `1`) after every action whose grace window had
466
+ // already elapsed. The safe window now only relaxes the rule
467
+ // temporarily for the action that JUST happened; it must be combined
468
+ // with a real focus element for digit-key shortcuts to fire.
469
+ const activeEl = document.activeElement;
470
+ const transientFocus = !activeEl || activeEl === document.body || activeEl === document.documentElement;
471
+ if (transientFocus) {
472
+ // Even within the grace window, body/null focus means there is
473
+ // no real input target — refuse to switch tabs. The user must
474
+ // explicitly click into a focusable element (input, button, etc.)
475
+ // to enable digit-key tab switching again.
476
+ return;
477
+ }
478
+ const target = e.target as HTMLElement | null;
479
+ const tag = target?.tagName?.toLowerCase();
480
+ const isFormControl =
481
+ tag === 'input' ||
482
+ tag === 'textarea' ||
483
+ tag === 'select' ||
484
+ tag === 'button' ||
485
+ tag === 'option' ||
486
+ tag === 'label' ||
487
+ !!target?.isContentEditable;
488
+ // Inside a form? Even safer: walk up to check.
489
+ let inForm = false;
490
+ if (target && typeof target.closest === 'function') {
491
+ inForm = !!target.closest('form, [role="dialog"], [contenteditable], [data-no-key]');
492
+ }
493
+ // v3.5.2 — reject key-repeat events. When a user presses and holds
494
+ // a digit key the browser fires repeated keydown events; we must only
495
+ // act on the first (e.repeat === false). This prevents the modal-close
496
+ // + key-repeat scenario from switching tabs after the safe window closes.
497
+ if (e.repeat) return;
498
+ // v3.6.1 — Check isModalOpenRef (not the closure variable) so we always
499
+ // see the current value even between state update and effect re-run.
500
+ if (isModalOpenRef.current) return;
501
+ if (
502
+ isFormControl ||
503
+ inForm ||
504
+ e.metaKey ||
505
+ e.ctrlKey ||
506
+ e.altKey ||
507
+ e.shiftKey ||
508
+ Date.now() < safeUntilRef.current
509
+ )
510
+ return;
511
+ const id = map[e.key];
512
+ if (id) {
513
+ e.preventDefault();
514
+ setActiveTab(id);
515
+ }
516
+ };
517
+ document.addEventListener('keydown', handler);
518
+ return () => document.removeEventListener('keydown', handler);
519
+ }, []);
520
+
521
+ const refreshSnapshot = useMemo(
522
+ () => async () => {
523
+ try {
524
+ const s = await api.get<Snapshot>('/snapshot');
525
+ setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s }));
526
+ } catch (err) {
527
+ toast.error(`Refresh failed: ${(err as Error).message}`);
528
+ }
529
+ },
530
+ [toast],
531
+ );
532
+
533
+ // v3.20.3 — Merge built-in tabs with installed mod views. Mod views
534
+ // are appended after the built-in tabs and use the same rendering
535
+ // surface; their tab ids are mod view ids (e.g. 'graphify:web').
536
+ // Declared AFTER refreshSnapshot so the deps list has the right value.
537
+ const mergedTabs = useMemo(() => {
538
+ const modTabs = modViews.map((v) => ({
539
+ id: v.id,
540
+ label: v.label,
541
+ icon: v.kind === 'iframe' ? Globe : LayoutTemplate,
542
+ isMod: true,
543
+ modId: v.modId,
544
+ }));
545
+ return [...TABS, ...modTabs];
546
+ }, [modViews]);
547
+
548
+ // Surface the underlying view for the active tab (built-in or mod).
549
+ // Declared AFTER refreshSnapshot so its useMemo factory can capture it.
550
+ const renderedView = useMemo(() => {
551
+ if (snapshot && settings) {
552
+ const viewProps: ViewProps = {
553
+ snapshot,
554
+ settings,
555
+ activeTab,
556
+ setActiveTab: setActiveTab,
557
+ refreshSnapshot,
558
+ };
559
+ return renderActiveView(activeTab, viewProps, modViews, modViewsReloadKey);
560
+ }
561
+ return null;
562
+ }, [activeTab, snapshot, settings, modViews, modViewsReloadKey, refreshSnapshot]);
563
+
564
+ const refreshProjects = async () => {
565
+ try {
566
+ const r = await api.get<{ projects: ProjectRecord[]; active: string | null }>('/projects');
567
+ setSnapshot((cur) =>
568
+ cur ? { ...cur, projects: r.projects || [], activeProject: r.projects?.find((p) => p.id === r.active) || null } : cur,
569
+ );
570
+ } catch (err) {
571
+ toast.error(`Projects refresh failed: ${(err as Error).message}`);
572
+ }
573
+ };
574
+
575
+ const onActivateProject = async (id: string) => {
576
+ try {
577
+ await api.post(`/projects/${encodeURIComponent(id)}/activate`);
578
+ // WS will fire project:change and refresh; but also reflect locally
579
+ setSnapshot((cur) => {
580
+ if (!cur) return cur;
581
+ return {
582
+ ...cur,
583
+ activeProject: cur.projects.find((p) => p.id === id) || null,
584
+ };
585
+ });
586
+ } catch (err) {
587
+ toast.error(`Activate failed: ${(err as Error).message}`);
588
+ }
589
+ };
590
+
591
+ const onSearchSelect = (r: SearchResult) => {
592
+ const t = r.type;
593
+ if (t === 'agent') setActiveTab('agents');
594
+ else if (t === 'task') setActiveTab('tasks');
595
+ else if (t === 'mod') setActiveTab('mods');
596
+ else if (t === 'schedule') setActiveTab('schedules');
597
+ else if (t === 'project') {
598
+ const id = (r.item as ProjectRecord).id;
599
+ onActivateProject(id);
600
+ } else if (t === 'command') {
601
+ // No command page; just toast
602
+ toast.info(`/${(r.item as { name: string }).name} — run from the TUI`, 2500);
603
+ } else if (t === 'setting') {
604
+ // v3.0.4 — Jump to Settings view and scroll to the matching row.
605
+ const settingId = (r.item as { id?: string; path?: string }).id
606
+ || (r.item as { path?: string }).path
607
+ || '';
608
+ setActiveTab('settings');
609
+ // Defer the scroll to allow the view to mount, then apply a brief
610
+ // CSS highlight. We poll a few times because the Settings view
611
+ // mounts lazily after tab switch.
612
+ const tryScroll = (tries: number) => {
613
+ if (tries <= 0) return;
614
+ const el = settingId
615
+ ? document.querySelector(`[data-setting-id="${CSS.escape(settingId)}"]`)
616
+ : null;
617
+ if (el) {
618
+ (el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
619
+ el.classList.remove('setting-flash');
620
+ // Force reflow so the animation re-triggers on rapid jumps.
621
+ void (el as HTMLElement).offsetWidth;
622
+ el.classList.add('setting-flash');
623
+ window.setTimeout(() => el.classList.remove('setting-flash'), 1500);
624
+ } else {
625
+ window.setTimeout(() => tryScroll(tries - 1), 80);
626
+ }
627
+ };
628
+ window.setTimeout(() => tryScroll(15), 60);
629
+ }
630
+ };
631
+
632
+ const layout = settings?.ui?.layout || 'topnav';
633
+ const showHeader = settings?.ui?.showHeader !== false;
634
+
635
+ return (
636
+ <div className="app" data-layout={layout} data-active-tab={activeTab}>
637
+ {showHeader && (
638
+ <Topbar
639
+ activeTab={activeTab}
640
+ onTabChange={setActiveTab}
641
+ wsStatus={wsStatus}
642
+ version={VERSION}
643
+ activeProject={snapshot?.activeProject || null}
644
+ projects={snapshot?.projects || []}
645
+ onProjectChange={onActivateProject}
646
+ onProjectsRefresh={refreshProjects}
647
+ onOpenSearch={() => setSearchOpen(true)}
648
+ settings={settings}
649
+ notificationsSlot={<Notifications wsSubscribe={subscribeToWs} />}
650
+ showTabs={layout === 'topnav'}
651
+ extraTabs={mergedTabs}
652
+ />
653
+ )}
654
+ {stuckAgents.length > 0 && !stuckBannerDismissed && (
655
+ <div className="stuck-banner" role="alert">
656
+ <AlertTriangle size={16} />
657
+ <span>
658
+ <strong>{stuckAgents.length}</strong> agent{stuckAgents.length === 1 ? '' : 's'} stuck:{' '}
659
+ <span className="mono">
660
+ {stuckAgents.map((a) => a.name).join(', ')}
661
+ </span>
662
+ </span>
663
+ <Button
664
+ variant="secondary"
665
+ size="sm"
666
+ onClick={() => {
667
+ setActiveTab('agents');
668
+ setStuckBannerDismissed(true);
669
+ }}
670
+ >
671
+ Review
672
+ </Button>
673
+ <button
674
+ type="button"
675
+ className="icon-btn"
676
+ aria-label="Dismiss"
677
+ onClick={() => setStuckBannerDismissed(true)}
678
+ >
679
+ <X size={14} />
680
+ </button>
681
+ </div>
682
+ )}
683
+ <div className="layout-body">
684
+ {layout !== 'topnav' && (
685
+ <Sidebar
686
+ tabs={mergedTabs}
687
+ activeTab={activeTab}
688
+ onTabChange={setActiveTab}
689
+ />
690
+ )}
691
+ <main className="content">
692
+ {bootError && (
693
+ <div className="boot-error">
694
+ <h2>Dashboard unavailable</h2>
695
+ <p>{bootError}</p>
696
+ <p className="boot-error-hint">
697
+ Make sure the Bizar dashboard server is running. Try{' '}
698
+ <code>bizar-dash start</code> in your terminal.
699
+ </p>
700
+ </div>
701
+ )}
702
+ {!bootError && (!snapshot || !settings) && (
703
+ <div className="loading">
704
+ <Spinner size="lg" />
705
+ <p>Loading Bizar…</p>
706
+ </div>
707
+ )}
708
+ {renderedView}
709
+ </main>
710
+ </div>
711
+ <SearchModal
712
+ open={searchOpen}
713
+ onClose={() => setSearchOpen(false)}
714
+ onSelect={onSearchSelect}
715
+ />
716
+ </div>
717
+ );
718
+ }