@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,633 @@
1
+ // src/components/FileBrowser.tsx — Interactive directory browser with tree + flat pane.
2
+ // Replaces raw path text inputs in Add Project dialogs.
3
+
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { ChevronRight, ChevronDown, FolderOpen, Folder, File, RefreshCw, ArrowLeft, Home, AlertCircle, FolderPlus } from 'lucide-react';
6
+ import { api } from '../lib/api';
7
+ import type { DirectoryEntry, DirectoryListing, MkdirRequest, MkdirResponse } from '../lib/types';
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ export type FileBrowserProps = {
12
+ /** Currently selected directory (absolute path). */
13
+ value: string;
14
+ /** Fires when the user selects a directory. */
15
+ onChange: (path: string) => void;
16
+ /** Where the browser starts (default: homedir via GET /api/fs with no path). */
17
+ initialPath?: string;
18
+ /** If set, used as default initialPath and shown as a quick-jump chip. */
19
+ projectsDirectory?: string;
20
+ /** Label for the filesystem-root breadcrumb (default: "Home"). */
21
+ rootLabel?: string;
22
+ /** Scroll viewport height in px (default: 360). */
23
+ height?: number;
24
+ };
25
+
26
+ // ─── Cache (LRU-ish, 200 entries max, 5-minute TTL) ───────────────────────────
27
+
28
+ const MAX_CACHE = 200;
29
+ const TTL_MS = 5 * 60 * 1000;
30
+
31
+ type CacheEntry = { data: DirectoryListing; ts: number };
32
+
33
+ const _cache = new Map<string, CacheEntry>();
34
+
35
+ function cacheGet(path: string): DirectoryListing | null {
36
+ const entry = _cache.get(path);
37
+ if (!entry) return null;
38
+ if (Date.now() - entry.ts > TTL_MS) { _cache.delete(path); return null; }
39
+ return entry.data;
40
+ }
41
+
42
+ function cacheSet(path: string, data: DirectoryListing) {
43
+ if (_cache.size >= MAX_CACHE) {
44
+ // Evict oldest 50
45
+ const oldest = [..._cache.entries()]
46
+ .sort((a, b) => a[1].ts - b[1].ts)
47
+ .slice(0, 50)
48
+ .map(([k]) => k);
49
+ oldest.forEach((k) => _cache.delete(k));
50
+ }
51
+ _cache.set(path, { data, ts: Date.now() });
52
+ }
53
+
54
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
55
+
56
+ function pathSegments(p: string): { label: string; path: string }[] {
57
+ if (!p || p === '/') return [{ label: '/', path: '/' }];
58
+ const parts = p.split('/').filter(Boolean);
59
+ const result: { label: string; path: string }[] = [];
60
+ let accumulated = '';
61
+ for (let i = 0; i < parts.length; i++) {
62
+ accumulated += '/' + parts[i];
63
+ result.push({ label: parts[i], path: accumulated });
64
+ }
65
+ return result;
66
+ }
67
+
68
+ function sortEntries(entries: DirectoryEntry[]): DirectoryEntry[] {
69
+ return [...entries].sort((a, b) => {
70
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
71
+ return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Validates a directory name against the same denylist the server enforces.
77
+ * Returns an ok object or { ok: false, reason }.
78
+ * Mirrors the validation in POST /api/fs/mkdir on the server side.
79
+ */
80
+ export function validateDirName(name: string): { ok: true } | { ok: false; reason: string } {
81
+ const trimmed = name.trim();
82
+ if (!trimmed) return { ok: false, reason: 'Name cannot be empty.' };
83
+ if (trimmed !== name) return { ok: false, reason: 'Name cannot have leading or trailing whitespace.' };
84
+ if (trimmed === '.') return { ok: false, reason: "Name cannot be '.'." };
85
+ if (trimmed === '..') return { ok: false, reason: "Name cannot be '..'." };
86
+ if (trimmed.startsWith('-')) return { ok: false, reason: "Name cannot start with '-'." };
87
+ if (trimmed.length > 255) return { ok: false, reason: 'Name cannot be longer than 255 characters.' };
88
+ if (/[/\0:*?"<>|]/.test(trimmed)) {
89
+ return { ok: false, reason: "Name cannot contain / \\ : * ? \" < > |" };
90
+ }
91
+ return { ok: true };
92
+ }
93
+
94
+ // ─── Component ─────────────────────────────────────────────────────────────────
95
+
96
+ export function FileBrowser({
97
+ value,
98
+ onChange,
99
+ initialPath,
100
+ projectsDirectory,
101
+ rootLabel = 'Home',
102
+ height = 360,
103
+ }: FileBrowserProps) {
104
+ const [currentPath, setCurrentPath] = useState<string>(
105
+ initialPath ?? projectsDirectory ?? '',
106
+ );
107
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
108
+ const [selectedRow, setSelectedRow] = useState<number>(-1);
109
+ const [listing, setListing] = useState<DirectoryListing | null>(null);
110
+ const [childrenMap, setChildrenMap] = useState<Record<string, DirectoryEntry[]>>({});
111
+ const [loading, setLoading] = useState(false);
112
+ const [error, setError] = useState<string | null>(null);
113
+ const [mkdirOpen, setMkdirOpen] = useState(false);
114
+ const [mkdirName, setMkdirName] = useState('');
115
+ const [mkdirError, setMkdirError] = useState<string | null>(null);
116
+ const [mkdirLoading, setMkdirLoading] = useState(false);
117
+ const mkdirInputRef = useRef<HTMLInputElement | null>(null);
118
+
119
+ const fetchListing = useCallback(async (path: string): Promise<DirectoryListing | null> => {
120
+ try {
121
+ const cached = cacheGet(path);
122
+ if (cached) return cached;
123
+ const data = await api.get<DirectoryListing>('/fs?path=' + encodeURIComponent(path));
124
+ cacheSet(path, data);
125
+ return data;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }, []);
130
+
131
+ const load = useCallback(async (path: string) => {
132
+ setLoading(true);
133
+ setError(null);
134
+ try {
135
+ const data = await api.get<DirectoryListing>('/fs?path=' + encodeURIComponent(path));
136
+ cacheSet(path, data);
137
+ setListing(data);
138
+ setSelectedRow(-1);
139
+ } catch (err) {
140
+ const msg = (err as { data?: { message?: string }; message?: string }).data?.message
141
+ ?? (err as Error).message
142
+ ?? 'Failed to load directory';
143
+ setError(msg);
144
+ } finally {
145
+ setLoading(false);
146
+ }
147
+ }, []);
148
+
149
+ // Load current path on mount or when currentPath changes
150
+ useEffect(() => {
151
+ if (!currentPath) {
152
+ // Default to homedir by fetching empty-path listing
153
+ api.get<DirectoryListing>('/fs').then((data) => {
154
+ cacheSet('', data);
155
+ setListing(data);
156
+ setCurrentPath(data.path);
157
+ if (data.path !== value) onChange(data.path);
158
+ }).catch(() => {
159
+ setError('Could not determine home directory.');
160
+ setLoading(false);
161
+ });
162
+ } else {
163
+ load(currentPath);
164
+ }
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
166
+ }, [currentPath]);
167
+
168
+ // Keep selected path in sync when value changes externally (e.g. breadcrumb click)
169
+ useEffect(() => {
170
+ if (value && value !== currentPath) {
171
+ setCurrentPath(value);
172
+ }
173
+ }, [value]);
174
+
175
+ const entries = listing ? sortEntries(listing.entries) : [];
176
+
177
+ const dirs = entries.filter((e) => e.isDir);
178
+ const allEntries = entries;
179
+
180
+ // ─── Navigation helpers ──────────────────────────────────────────────────────
181
+
182
+ const navigateInto = (entry: DirectoryEntry) => {
183
+ if (!entry.isDir) return;
184
+ setCurrentPath(entry.path);
185
+ onChange(entry.path);
186
+ };
187
+
188
+ const navigateUp = () => {
189
+ if (!listing?.parent) return;
190
+ setCurrentPath(listing.parent);
191
+ onChange(listing.parent);
192
+ };
193
+
194
+ const navigateTo = (path: string) => {
195
+ if (path === currentPath) return;
196
+ setCurrentPath(path);
197
+ onChange(path);
198
+ };
199
+
200
+ const goHome = async () => {
201
+ const home = await fetchListing('');
202
+ if (home) {
203
+ setCurrentPath(home.path);
204
+ onChange(home.path);
205
+ }
206
+ };
207
+
208
+ const goProjects = async () => {
209
+ if (!projectsDirectory) return;
210
+ const pd = await fetchListing(projectsDirectory);
211
+ if (pd) {
212
+ setCurrentPath(pd.path);
213
+ onChange(pd.path);
214
+ }
215
+ };
216
+
217
+ // ─── New folder ────────────────────────────────────────────────────────────
218
+
219
+ // Open the inline mkdir input, focusing it on the next tick
220
+ const openMkdir = () => {
221
+ setMkdirOpen(true);
222
+ setMkdirName('');
223
+ setMkdirError(null);
224
+ // Autofocus after the input renders
225
+ setTimeout(() => mkdirInputRef.current?.focus(), 0);
226
+ };
227
+
228
+ const cancelMkdir = () => {
229
+ setMkdirOpen(false);
230
+ setMkdirName('');
231
+ setMkdirError(null);
232
+ };
233
+
234
+ const submitMkdir = async () => {
235
+ const v = validateDirName(mkdirName);
236
+ if (!v.ok) {
237
+ setMkdirError(v.reason);
238
+ return;
239
+ }
240
+ setMkdirLoading(true);
241
+ setMkdirError(null);
242
+ try {
243
+ const result = await api.post<MkdirResponse>('/fs/mkdir', {
244
+ parent: currentPath,
245
+ name: mkdirName.trim(),
246
+ } as MkdirRequest);
247
+ // Invalidate cache for the current path so the new folder appears
248
+ _cache.delete(currentPath);
249
+ await load(currentPath);
250
+ // Select the new directory
251
+ onChange(result.path);
252
+ setMkdirOpen(false);
253
+ setMkdirName('');
254
+ } catch (err) {
255
+ const apiErr = err as { data?: { message?: string }; status?: number };
256
+ if (apiErr.status === 409) {
257
+ setMkdirError(`A folder named "${mkdirName.trim()}" already exists.`);
258
+ } else {
259
+ setMkdirError(
260
+ apiErr.data?.message ??
261
+ (err as Error).message ??
262
+ 'Failed to create folder.',
263
+ );
264
+ }
265
+ } finally {
266
+ setMkdirLoading(false);
267
+ }
268
+ };
269
+
270
+ const handleMkdirKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
271
+ if (e.key === 'Enter') {
272
+ e.preventDefault();
273
+ submitMkdir();
274
+ } else if (e.key === 'Escape') {
275
+ e.preventDefault();
276
+ cancelMkdir();
277
+ }
278
+ };
279
+
280
+ const toggleExpand = (path: string) => {
281
+ setExpanded((prev) => {
282
+ const next = new Set(prev);
283
+ if (next.has(path)) next.delete(path);
284
+ else next.add(path);
285
+ return next;
286
+ });
287
+ // Lazy-load children when expanding
288
+ if (!childrenMap[path]) {
289
+ fetchListing(path).then((data) => {
290
+ if (data) setChildrenMap((prev) => ({ ...prev, [path]: data.entries.filter((e) => e.isDir) }));
291
+ });
292
+ }
293
+ };
294
+
295
+ // ─── Keyboard handler ────────────────────────────────────────────────────────
296
+
297
+ const handleKeyDown = (e: React.KeyboardEvent) => {
298
+ if (!listing) return;
299
+ const dirs = allEntries.filter((x) => x.isDir);
300
+ const files = allEntries.filter((x) => !x.isDir);
301
+ const selectable = [...dirs, ...files];
302
+
303
+ if (e.key === 'ArrowDown') {
304
+ e.preventDefault();
305
+ setSelectedRow((r) => Math.min(r + 1, selectable.length - 1));
306
+ } else if (e.key === 'ArrowUp') {
307
+ e.preventDefault();
308
+ setSelectedRow((r) => Math.max(r - 1, 0));
309
+ } else if (e.key === 'Enter' || e.key === ' ') {
310
+ e.preventDefault();
311
+ const sel = selectable[selectedRow];
312
+ if (sel) {
313
+ if (e.key === 'Enter' && sel.isDir) navigateInto(sel);
314
+ else if (!sel.isDir) { /* files are not selectable */ }
315
+ else onChange(sel.path);
316
+ }
317
+ } else if (e.key === 'Backspace') {
318
+ e.preventDefault();
319
+ navigateUp();
320
+ }
321
+ };
322
+
323
+ // ─── Render ─────────────────────────────────────────────────────────────────
324
+
325
+ const segments = pathSegments(listing?.path ?? currentPath ?? '/');
326
+ const parentPath = listing?.parent;
327
+ const canGoUp = parentPath !== null && parentPath !== undefined;
328
+
329
+ const basename = currentPath.split('/').filter(Boolean).at(-1) ?? '/';
330
+
331
+ return (
332
+ <div className="file-browser" onKeyDown={handleKeyDown} tabIndex={-1}>
333
+ {/* Breadcrumb */}
334
+ <div className="file-browser-breadcrumb" role="navigation" aria-label="Path breadcrumb">
335
+ {segments.map((seg, i) => (
336
+ <span key={seg.path} className="file-browser-breadcrumb-item">
337
+ {i > 0 && <span className="file-browser-breadcrumb-sep" aria-hidden>/</span>}
338
+ <button
339
+ type="button"
340
+ className="file-browser-breadcrumb-btn"
341
+ onClick={() => navigateTo(seg.path)}
342
+ title={seg.path}
343
+ >
344
+ {i === 0 ? (seg.path === '/' ? '/' : rootLabel) : seg.label}
345
+ </button>
346
+ </span>
347
+ ))}
348
+ </div>
349
+
350
+ {/* Toolbar */}
351
+ <div className="file-browser-toolbar">
352
+ <div className="file-browser-toolbar-left">
353
+ <button
354
+ type="button"
355
+ className="file-browser-tool-btn"
356
+ onClick={navigateUp}
357
+ disabled={!canGoUp}
358
+ title="Go up (Backspace)"
359
+ aria-label="Go up one level"
360
+ >
361
+ <ArrowLeft size={13} />
362
+ </button>
363
+
364
+ <button
365
+ type="button"
366
+ className="file-browser-tool-btn"
367
+ onClick={() => load(currentPath)}
368
+ disabled={loading}
369
+ title="Refresh"
370
+ aria-label="Refresh"
371
+ >
372
+ <RefreshCw size={13} className={loading ? 'spin' : ''} />
373
+ </button>
374
+
375
+ <button
376
+ type="button"
377
+ className="file-browser-tool-btn"
378
+ onClick={openMkdir}
379
+ title="New folder"
380
+ aria-label="Create new folder"
381
+ >
382
+ <FolderPlus size={13} />
383
+ </button>
384
+
385
+ <div className="file-browser-chips">
386
+ <button type="button" className="file-browser-chip" onClick={goHome}>
387
+ <Home size={11} /> {rootLabel}
388
+ </button>
389
+ {projectsDirectory && (
390
+ <button type="button" className="file-browser-chip" onClick={goProjects} title={projectsDirectory}>
391
+ <FolderOpen size={11} /> {projectsDirectory.split('/').filter(Boolean).at(-1) ?? 'Projects'}
392
+ </button>
393
+ )}
394
+ </div>
395
+ </div>
396
+
397
+ {mkdirOpen ? (
398
+ <div className="file-browser-mkdir">
399
+ <input
400
+ ref={mkdirInputRef}
401
+ type="text"
402
+ className="file-browser-mkdir-input"
403
+ placeholder="Folder name"
404
+ value={mkdirName}
405
+ onChange={(e) => {
406
+ setMkdirName(e.target.value);
407
+ setMkdirError(null);
408
+ }}
409
+ onKeyDown={handleMkdirKeyDown}
410
+ aria-label="New folder name"
411
+ aria-invalid={mkdirError ? 'true' : undefined}
412
+ aria-describedby={mkdirError ? 'mkdir-error' : undefined}
413
+ disabled={mkdirLoading}
414
+ maxLength={255}
415
+ />
416
+ {mkdirError ? (
417
+ <span id="mkdir-error" className="file-browser-mkdir-error" role="alert">
418
+ {mkdirError}
419
+ </span>
420
+ ) : (
421
+ <span className="file-browser-mkdir-hint">
422
+ Enter to create, Esc to cancel
423
+ </span>
424
+ )}
425
+ </div>
426
+ ) : (
427
+ <span className="file-browser-count-hint">
428
+ {loading ? '…' : `${entries.length} in ${basename}`}
429
+ </span>
430
+ )}
431
+ </div>
432
+
433
+ {/* Error banner */}
434
+ {error && (
435
+ <div className="file-browser-error">
436
+ <AlertCircle size={13} />
437
+ <span>{error}</span>
438
+ <button type="button" className="file-browser-retry" onClick={() => load(currentPath)}>
439
+ Retry
440
+ </button>
441
+ </div>
442
+ )}
443
+
444
+ {/* Body: tree (left) + flat (right) */}
445
+ <div className="file-browser-body" style={{ height }}>
446
+ {/* Left: tree */}
447
+ <div className="file-browser-pane file-browser-tree" aria-label="Folder tree">
448
+ {loading && !listing ? (
449
+ <TreeSkeleton />
450
+ ) : (
451
+ <TreeNode
452
+ path={currentPath}
453
+ entries={dirs}
454
+ expanded={expanded}
455
+ childrenMap={childrenMap}
456
+ onToggle={toggleExpand}
457
+ onNavigate={navigateInto}
458
+ level={0}
459
+ />
460
+ )}
461
+ </div>
462
+
463
+ {/* Right: flat listing */}
464
+ <div className="file-browser-pane file-browser-flat" aria-label="Directory contents">
465
+ <div className="file-browser-flat-header">
466
+ <span>Name</span>
467
+ <span>Type</span>
468
+ </div>
469
+
470
+ {loading && !listing ? (
471
+ <FlatSkeleton />
472
+ ) : (
473
+ <>
474
+ {/* Truncation notice */}
475
+ {listing?.truncated && (
476
+ <div className="file-browser-truncated-banner" role="status">
477
+ Showing first 500 of {listing.totalEntries} entries.
478
+ Navigate into a subfolder to see more.
479
+ </div>
480
+ )}
481
+
482
+ {allEntries.length === 0 ? (
483
+ <div className="file-browser-empty">This folder is empty</div>
484
+ ) : (
485
+ <div className="file-browser-flat-list">
486
+ {allEntries.map((entry, idx) => {
487
+ const isDir = entry.isDir;
488
+ const isSelected = idx === selectedRow;
489
+ const isActive = entry.path === value;
490
+ return (
491
+ <div
492
+ key={entry.path}
493
+ role="option"
494
+ aria-selected={isActive}
495
+ className={[
496
+ 'file-browser-row',
497
+ isDir ? 'file-browser-row--dir' : 'file-browser-row--file',
498
+ isSelected && 'file-browser-row--selected',
499
+ isActive && isDir && 'file-browser-row--active',
500
+ !isDir && 'file-browser-row--disabled',
501
+ ].filter(Boolean).join(' ')}
502
+ onClick={() => {
503
+ if (isDir) {
504
+ setSelectedRow(idx);
505
+ onChange(entry.path);
506
+ }
507
+ }}
508
+ onDoubleClick={() => {
509
+ if (isDir) navigateInto(entry);
510
+ }}
511
+ title={entry.path}
512
+ >
513
+ <span className="file-browser-row-icon">
514
+ {isDir ? <Folder size={13} /> : <File size={13} />}
515
+ </span>
516
+ <span className="file-browser-row-name">{entry.name}</span>
517
+ <span className="file-browser-row-type">
518
+ {isDir ? 'Folder' : ''}
519
+ </span>
520
+ </div>
521
+ );
522
+ })}
523
+ </div>
524
+ )}
525
+ </>
526
+ )}
527
+ </div>
528
+ </div>
529
+
530
+ {/* Footer hint */}
531
+ <div className="file-browser-footer-hint">
532
+ Select a folder, then click Add. Use <kbd>↑</kbd> <kbd>↓</kbd> to navigate, <kbd>Enter</kbd> to confirm.
533
+ </div>
534
+ </div>
535
+ );
536
+ }
537
+
538
+ // ─── TreeNode ─────────────────────────────────────────────────────────────────
539
+
540
+ function TreeNode({
541
+ path,
542
+ entries,
543
+ expanded,
544
+ childrenMap,
545
+ onToggle,
546
+ onNavigate,
547
+ level,
548
+ }: {
549
+ path: string;
550
+ entries: DirectoryEntry[];
551
+ expanded: Set<string>;
552
+ childrenMap: Record<string, DirectoryEntry[]>;
553
+ onToggle: (p: string) => void;
554
+ onNavigate: (e: DirectoryEntry) => void;
555
+ level: number;
556
+ }) {
557
+ return (
558
+ <ul className="file-browser-tree-list" role="group">
559
+ {entries.map((entry) => {
560
+ const isExpanded = expanded.has(entry.path);
561
+ const kids = childrenMap[entry.path] ?? [];
562
+ return (
563
+ <li key={entry.path} className="file-browser-tree-node">
564
+ <div
565
+ className={[
566
+ 'file-browser-tree-row',
567
+ isExpanded && 'file-browser-tree-row--expanded',
568
+ ].filter(Boolean).join(' ')}
569
+ style={{ paddingLeft: `${level * 16 + 8}px` }}
570
+ >
571
+ <button
572
+ type="button"
573
+ className="file-browser-tree-toggle"
574
+ onClick={(e) => { e.stopPropagation(); onToggle(entry.path); }}
575
+ aria-label={isExpanded ? 'Collapse' : 'Expand'}
576
+ >
577
+ {kids.length > 0 || true ? (
578
+ isExpanded ? <ChevronDown size={11} /> : <ChevronRight size={11} />
579
+ ) : (
580
+ <span style={{ width: 11 }} />
581
+ )}
582
+ </button>
583
+
584
+ <button
585
+ type="button"
586
+ className="file-browser-tree-name"
587
+ onClick={() => onNavigate(entry)}
588
+ title={entry.path}
589
+ >
590
+ <Folder size={12} />
591
+ <span>{entry.name}</span>
592
+ </button>
593
+ </div>
594
+
595
+ {isExpanded && kids.length > 0 && (
596
+ <TreeNode
597
+ path={entry.path}
598
+ entries={kids}
599
+ expanded={expanded}
600
+ childrenMap={childrenMap}
601
+ onToggle={onToggle}
602
+ onNavigate={onNavigate}
603
+ level={level + 1}
604
+ />
605
+ )}
606
+ </li>
607
+ );
608
+ })}
609
+ </ul>
610
+ );
611
+ }
612
+
613
+ // ─── Skeletons ────────────────────────────────────────────────────────────────
614
+
615
+ function TreeSkeleton() {
616
+ return (
617
+ <div className="file-browser-skeleton-wrap">
618
+ {[80, 60, 90, 55, 70].map((w, i) => (
619
+ <div key={i} className="file-browser-skeleton-row" style={{ width: w }} />
620
+ ))}
621
+ </div>
622
+ );
623
+ }
624
+
625
+ function FlatSkeleton() {
626
+ return (
627
+ <div className="file-browser-skeleton-wrap">
628
+ {[60, 90, 70, 50, 80, 65].map((w, i) => (
629
+ <div key={i} className="file-browser-skeleton-row" style={{ width: w }} />
630
+ ))}
631
+ </div>
632
+ );
633
+ }
@@ -0,0 +1,55 @@
1
+ // src/components/HelpDialog.tsx — Dialog for /help and /bizar commands.
2
+
3
+ import { Button } from './Button';
4
+
5
+ type HelpDialogProps = {
6
+ data?: Record<string, unknown>;
7
+ onClose: () => void;
8
+ };
9
+
10
+ type CmdEntry = { cmd: string; desc: string };
11
+
12
+ export function HelpDialog({ data, onClose }: HelpDialogProps) {
13
+ const commands = (data?.commands as CmdEntry[]) ?? [];
14
+ const templates = (data?.templates as string[]) ?? [];
15
+ const statuses = (data?.statuses as string[]) ?? [];
16
+
17
+ return (
18
+ <div>
19
+ {commands.length > 0 ? (
20
+ <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 16, fontSize: 13 }}>
21
+ <tbody>
22
+ {commands.map((entry) => (
23
+ <tr key={entry.cmd} style={{ borderBottom: '1px solid var(--color-border)' }}>
24
+ <td style={{ padding: '5px 8px 5px 0', whiteSpace: 'nowrap' }}>
25
+ <code style={{ fontSize: 12 }}>{entry.cmd}</code>
26
+ </td>
27
+ <td style={{ padding: '5px 0', color: 'var(--color-muted)' }}>{entry.desc}</td>
28
+ </tr>
29
+ ))}
30
+ </tbody>
31
+ </table>
32
+ ) : (
33
+ <p style={{ marginBottom: 16, color: 'var(--color-muted)' }}>No commands available.</p>
34
+ )}
35
+
36
+ {templates.length > 0 && (
37
+ <div style={{ marginBottom: 12 }}>
38
+ <p style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 4 }}>Available templates:</p>
39
+ <p style={{ fontSize: 13 }}>{templates.join(', ')}</p>
40
+ </div>
41
+ )}
42
+
43
+ {statuses.length > 0 && (
44
+ <div style={{ marginBottom: 12 }}>
45
+ <p style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 4 }}>Available statuses:</p>
46
+ <p style={{ fontSize: 13 }}>{statuses.join(', ')}</p>
47
+ </div>
48
+ )}
49
+
50
+ <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
51
+ <Button variant="ghost" onClick={onClose}>Close</Button>
52
+ </div>
53
+ </div>
54
+ );
55
+ }