@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,450 @@
1
+ // src/views/BackgroundAgents.tsx — dashboard tab listing active background-agent
2
+ // instances (status === 'pending' | 'running'). Polls /api/background every 5s
3
+ // as a safety net and refreshes on `background:change` WS events for near-real-time
4
+ // updates. Per-instance actions: View live output (auto-refreshing modal), Open
5
+ // in tmux (shows attach command), Kill (with confirm dialog).
6
+ import { useCallback, useEffect, useRef, useState, type UIEvent } from 'react';
7
+ import {
8
+ Activity as ActivityIcon,
9
+ Copy,
10
+ Eye,
11
+ Loader2,
12
+ RefreshCw,
13
+ Terminal,
14
+ Trash2,
15
+ } from 'lucide-react';
16
+ import { Button } from '../components/Button';
17
+ import { Card } from '../components/Card';
18
+ import { EmptyState } from '../components/EmptyState';
19
+ import { Spinner } from '../components/Spinner';
20
+ import { useModal } from '../components/Modal';
21
+ import { useToast } from '../components/Toast';
22
+ import { BgStatusBadge } from '../components/BgStatusBadge';
23
+ import { openKillConfirmDialog } from '../components/KillConfirmDialog';
24
+ import { api } from '../lib/api';
25
+ import { Ws } from '../lib/ws';
26
+ import { cn, formatTime, truncate } from '../lib/utils';
27
+ import type {
28
+ BackgroundListResponse,
29
+ BackgroundOutputResponse,
30
+ BackgroundTmuxResponse,
31
+ BgInstance,
32
+ Settings,
33
+ Snapshot,
34
+ } from '../lib/types';
35
+
36
+ type Props = {
37
+ snapshot: Snapshot;
38
+ settings: Settings;
39
+ activeTab: string;
40
+ setActiveTab: (id: string) => void;
41
+ refreshSnapshot: () => Promise<void>;
42
+ };
43
+
44
+ const POLL_INTERVAL_MS = 5_000;
45
+ const OUTPUT_REFRESH_MS = 2_000;
46
+ const PROMPT_TRUNCATE = 120;
47
+
48
+ function isActive(b: BgInstance): boolean {
49
+ return b.status === 'pending' || b.status === 'running';
50
+ }
51
+
52
+ function formatDuration(ms: number): string {
53
+ if (!Number.isFinite(ms) || ms < 0) return '—';
54
+ const total = Math.floor(ms / 1000);
55
+ const h = Math.floor(total / 3600);
56
+ const m = Math.floor((total % 3600) / 60);
57
+ const s = total % 60;
58
+ if (h > 0) return `${h}h ${m}m ${s}s`;
59
+ if (m > 0) return `${m}m ${s}s`;
60
+ return `${s}s`;
61
+ }
62
+
63
+ export function BackgroundAgents(_props: Props) {
64
+ const toast = useToast();
65
+ const modal = useModal();
66
+ const [instances, setInstances] = useState<BgInstance[]>([]);
67
+ const [loading, setLoading] = useState(true);
68
+ const [error, setError] = useState<string | null>(null);
69
+ const [, setTick] = useState(0);
70
+
71
+ const load = useCallback(async () => {
72
+ try {
73
+ const res = await api.get<BackgroundListResponse>('/background');
74
+ setInstances(Array.isArray(res?.instances) ? res.instances : []);
75
+ setError(null);
76
+ } catch (err) {
77
+ setError((err as Error)?.message || 'Failed to load background agents.');
78
+ } finally {
79
+ setLoading(false);
80
+ }
81
+ }, []);
82
+
83
+ // Initial load + polling safety net.
84
+ useEffect(() => {
85
+ load();
86
+ const id = setInterval(load, POLL_INTERVAL_MS);
87
+ return () => clearInterval(id);
88
+ }, [load]);
89
+
90
+ // 1s tick to keep the duration strings fresh.
91
+ useEffect(() => {
92
+ const id = setInterval(() => setTick((t) => (t + 1) % 1_000_000), 1000);
93
+ return () => clearInterval(id);
94
+ }, []);
95
+
96
+ // WS subscription — refetch on any background:change / cleanup event.
97
+ // Matches the Settings.tsx pattern of opening a private WS instance.
98
+ // Acceptable because background change volume is low and the dashboard
99
+ // already creates parallel WS connections for other tabs.
100
+ useEffect(() => {
101
+ const ws = new Ws();
102
+ const off = ws.on((msg) => {
103
+ const t = (msg as { type?: string }).type;
104
+ if (t === 'background:change' || t === 'background:cleanup') {
105
+ load();
106
+ }
107
+ });
108
+ return () => {
109
+ off();
110
+ ws.close();
111
+ };
112
+ }, [load]);
113
+
114
+ const active = instances.filter(isActive);
115
+
116
+ // ─── Handlers ─────────────────────────────────────────────────────────
117
+
118
+ const onOpenOutput = (b: BgInstance) => {
119
+ openOutputModal(modal, b);
120
+ };
121
+
122
+ const onOpenTmux = async (b: BgInstance) => {
123
+ try {
124
+ const info = await api.get<BackgroundTmuxResponse>(
125
+ `/background/${encodeURIComponent(b.instanceId)}/tmux`,
126
+ );
127
+ openTmuxModal(modal, info);
128
+ } catch (err) {
129
+ toast.error(`Tmux info failed: ${(err as Error).message}`);
130
+ }
131
+ };
132
+
133
+ const onKill = (b: BgInstance) => {
134
+ openKillConfirmDialog(modal, toast, b.instanceId, b.agent || b.prompt, () => {
135
+ // Optimistic remove — the WS broadcast + polling safety net will reconcile.
136
+ setInstances((cur) => cur.filter((x) => x.instanceId !== b.instanceId));
137
+ });
138
+ };
139
+
140
+ // ─── Render ───────────────────────────────────────────────────────────
141
+
142
+ if (loading) {
143
+ return (
144
+ <div className="bg-active-view">
145
+ <div className="bg-active-loading">
146
+ <Spinner size="md" />
147
+ <span>Loading background agents…</span>
148
+ </div>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ if (error) {
154
+ return (
155
+ <div className="bg-active-view">
156
+ <EmptyState
157
+ icon={<ActivityIcon size={32} />}
158
+ title="Couldn't load background agents"
159
+ message={error}
160
+ action={
161
+ <Button variant="primary" onClick={load}>
162
+ <RefreshCw size={14} /> Retry
163
+ </Button>
164
+ }
165
+ />
166
+ </div>
167
+ );
168
+ }
169
+
170
+ return (
171
+ <div className="bg-active-view">
172
+ <div className="bg-active-header">
173
+ <div className="bg-active-header-text">
174
+ <h2 className="bg-active-title">Active Background Agents</h2>
175
+ <p className="bg-active-subtitle muted">
176
+ {active.length} active · {instances.length} total in store
177
+ </p>
178
+ </div>
179
+ <Button variant="ghost" size="sm" onClick={load} title="Refresh now">
180
+ <RefreshCw size={14} /> Refresh
181
+ </Button>
182
+ </div>
183
+
184
+ {active.length === 0 ? (
185
+ <EmptyState
186
+ icon={<ActivityIcon size={32} />}
187
+ title="No active background agents"
188
+ message="Spawn one from the Agents tab or via the bizar_spawn_background tool."
189
+ />
190
+ ) : (
191
+ <div className="bg-active-list">
192
+ {active.map((b) => (
193
+ <BackgroundAgentCard
194
+ key={b.instanceId}
195
+ inst={b}
196
+ onViewOutput={() => onOpenOutput(b)}
197
+ onOpenTmux={() => onOpenTmux(b)}
198
+ onKill={() => onKill(b)}
199
+ />
200
+ ))}
201
+ </div>
202
+ )}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ // ─── Per-instance card ─────────────────────────────────────────────────────
208
+
209
+ function BackgroundAgentCard({
210
+ inst,
211
+ onViewOutput,
212
+ onOpenTmux,
213
+ onKill,
214
+ }: {
215
+ inst: BgInstance;
216
+ onViewOutput: () => void;
217
+ onOpenTmux: () => void;
218
+ onKill: () => void;
219
+ }) {
220
+ const startedMs = typeof inst.startedAt === 'number' ? inst.startedAt : 0;
221
+ const startedLabel = startedMs ? formatTime(startedMs) : '—';
222
+ const durationMs = startedMs ? Date.now() - startedMs : 0;
223
+ const prompt = truncate(inst.prompt, PROMPT_TRUNCATE);
224
+
225
+ return (
226
+ <Card className="bg-active-card" variant="elevated">
227
+ <div className="bg-active-card-top">
228
+ <div className="bg-active-card-id">
229
+ <code className="mono">{inst.instanceId}</code>
230
+ <BgStatusBadge status={inst.status || 'pending'} dot />
231
+ </div>
232
+ <div className="bg-active-card-agent">
233
+ <span className="bg-active-card-agent-label">Agent</span>
234
+ <span className="bg-active-card-agent-name">{inst.agent || 'unknown'}</span>
235
+ </div>
236
+ </div>
237
+
238
+ {prompt && <div className="bg-active-prompt">{prompt}</div>}
239
+
240
+ {inst.currentStep && (
241
+ <div className="bg-active-step">
242
+ <span className="bg-active-step-label">Step:</span>
243
+ <span className="bg-active-step-value">{inst.currentStep}</span>
244
+ </div>
245
+ )}
246
+
247
+ {typeof inst.progress === 'number' && (
248
+ <div className="progress-bar" aria-label="Progress">
249
+ <div
250
+ className="progress-fill"
251
+ style={{ width: `${Math.max(0, Math.min(100, inst.progress))}%` }}
252
+ />
253
+ <span className="progress-label">{Math.round(inst.progress)}%</span>
254
+ </div>
255
+ )}
256
+
257
+ <div className="bg-active-meta">
258
+ <span title={startedLabel}>
259
+ <strong>Started:</strong> {startedLabel}
260
+ </span>
261
+ <span>
262
+ <strong>Duration:</strong> {formatDuration(durationMs)}
263
+ </span>
264
+ <span>
265
+ <strong>Tool calls:</strong> {inst.toolCallCount ?? 0}
266
+ </span>
267
+ {inst.tmuxSession && (
268
+ <span className={cn(inst.tmuxActive && 'bg-active-meta-tmux-live')}>
269
+ <strong>tmux:</strong> <code className="mono">{inst.tmuxSession}</code>
270
+ {inst.tmuxActive ? ' · live' : ''}
271
+ </span>
272
+ )}
273
+ </div>
274
+
275
+ <div className="bg-active-actions">
276
+ <Button variant="secondary" size="sm" onClick={onViewOutput}>
277
+ <Eye size={14} /> View output
278
+ </Button>
279
+ <Button variant="ghost" size="sm" onClick={onOpenTmux}>
280
+ <Terminal size={14} /> Open in tmux
281
+ </Button>
282
+ <Button variant="danger" size="sm" onClick={onKill}>
283
+ <Trash2 size={14} /> Kill
284
+ </Button>
285
+ </div>
286
+ </Card>
287
+ );
288
+ }
289
+
290
+ // ─── Output modal (auto-refresh) ───────────────────────────────────────────
291
+
292
+ function openOutputModal(
293
+ modal: ReturnType<typeof useModal>,
294
+ inst: BgInstance,
295
+ ) {
296
+ modal.open({
297
+ title: `Output — ${inst.agent || inst.instanceId}`,
298
+ width: 720,
299
+ children: <OutputModalBody instanceId={inst.instanceId} />,
300
+ });
301
+ }
302
+
303
+ function OutputModalBody({ instanceId }: { instanceId: string }) {
304
+ const toast = useToast();
305
+ const [output, setOutput] = useState<string>('');
306
+ const [loading, setLoading] = useState(true);
307
+ const [error, setError] = useState<string | null>(null);
308
+ const [available, setAvailable] = useState(true);
309
+ const refBottom = useRef<HTMLDivElement | null>(null);
310
+ const refAutoscroll = useRef(true);
311
+
312
+ const load = useCallback(async () => {
313
+ try {
314
+ const r = await api.get<BackgroundOutputResponse>(
315
+ `/background/${encodeURIComponent(instanceId)}/output?lines=200`,
316
+ );
317
+ setOutput(r?.output ?? '');
318
+ setAvailable(r?.available !== false);
319
+ setError(null);
320
+ } catch (err) {
321
+ setError((err as Error)?.message || 'Failed to load output.');
322
+ } finally {
323
+ setLoading(false);
324
+ }
325
+ }, [instanceId]);
326
+
327
+ useEffect(() => {
328
+ load();
329
+ const id = setInterval(load, OUTPUT_REFRESH_MS);
330
+ return () => clearInterval(id);
331
+ }, [load]);
332
+
333
+ // Auto-scroll to bottom only if user is already near the bottom.
334
+ useEffect(() => {
335
+ if (!refAutoscroll.current) return;
336
+ refBottom.current?.scrollIntoView({ behavior: 'auto', block: 'end' });
337
+ }, [output]);
338
+
339
+ const onScroll = (e: UIEvent<HTMLDivElement>) => {
340
+ const el = e.currentTarget;
341
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
342
+ refAutoscroll.current = atBottom;
343
+ };
344
+
345
+ const copy = async () => {
346
+ try {
347
+ await navigator.clipboard.writeText(output);
348
+ toast.success('Output copied.');
349
+ } catch {
350
+ // ignore — clipboard may be unavailable in some contexts
351
+ }
352
+ };
353
+
354
+ return (
355
+ <div className="bg-output-modal">
356
+ <div className="bg-output-toolbar">
357
+ <span className="muted">
358
+ {available ? `${output.length} chars · auto-refresh 2s` : 'Output unavailable'}
359
+ </span>
360
+ <div className="bg-output-toolbar-actions">
361
+ <Button variant="ghost" size="sm" onClick={load} title="Refresh now">
362
+ <RefreshCw size={14} /> Refresh
363
+ </Button>
364
+ <Button variant="ghost" size="sm" onClick={copy} disabled={!output}>
365
+ <Copy size={14} /> Copy
366
+ </Button>
367
+ </div>
368
+ </div>
369
+ {loading && !output ? (
370
+ <div className="bg-output-loading">
371
+ <Loader2 size={16} className="spin" /> Loading…
372
+ </div>
373
+ ) : error ? (
374
+ <div className="bg-output-error">{error}</div>
375
+ ) : !output ? (
376
+ <div className="bg-output-empty muted">No output captured yet.</div>
377
+ ) : (
378
+ <div className="bg-output-scroll" onScroll={onScroll}>
379
+ <pre className="bg-output-pre mono">
380
+ <code>{output}</code>
381
+ </pre>
382
+ <div ref={refBottom} />
383
+ </div>
384
+ )}
385
+ </div>
386
+ );
387
+ }
388
+
389
+ // ─── Tmux attach modal ─────────────────────────────────────────────────────
390
+
391
+ function openTmuxModal(
392
+ modal: ReturnType<typeof useModal>,
393
+ info: BackgroundTmuxResponse,
394
+ ) {
395
+ const cmd = info.attachCommand || info.session || '';
396
+ modal.open({
397
+ title: 'Attach to tmux',
398
+ width: 520,
399
+ children: <TmuxModalBody info={info} cmd={cmd} />,
400
+ });
401
+ }
402
+
403
+ function TmuxModalBody({ info, cmd }: { info: BackgroundTmuxResponse; cmd: string }) {
404
+ const [copied, setCopied] = useState(false);
405
+ const onCopy = async () => {
406
+ try {
407
+ await navigator.clipboard.writeText(cmd);
408
+ setCopied(true);
409
+ window.setTimeout(() => setCopied(false), 1500);
410
+ } catch {
411
+ /* noop */
412
+ }
413
+ };
414
+
415
+ return (
416
+ <div className="bg-tmux-modal">
417
+ {!cmd ? (
418
+ <p className="muted">No tmux session attached to this instance.</p>
419
+ ) : (
420
+ <>
421
+ <p className="bg-tmux-meta">
422
+ {info.exists ? (
423
+ <span className="badge badge-success">session live</span>
424
+ ) : (
425
+ <span className="badge badge-warning">
426
+ session not running{info.reason ? ` — ${info.reason}` : ''}
427
+ </span>
428
+ )}
429
+ {info.session && (
430
+ <span className="bg-tmux-session muted">
431
+ · session <code className="mono">{info.session}</code>
432
+ </span>
433
+ )}
434
+ </p>
435
+ <label className="field-label">Attach command</label>
436
+ <div className="bg-tmux-command-row">
437
+ <code className="bg-tmux-command mono">{cmd}</code>
438
+ <Button variant="secondary" size="sm" onClick={onCopy}>
439
+ <Copy size={14} /> {copied ? 'Copied' : 'Copy'}
440
+ </Button>
441
+ </div>
442
+ <p className="muted bg-tmux-hint">
443
+ Paste this into a terminal on the host running the dashboard. The
444
+ dashboard itself does not open terminals.
445
+ </p>
446
+ </>
447
+ )}
448
+ </div>
449
+ );
450
+ }
@@ -0,0 +1,190 @@
1
+ // src/views/Chat.tsx — Gemini-inspired dark chat orchestrator.
2
+ // Composer state (agent, model, text, attachments) is local.
3
+ // useChat handles messages, sessions, send, polling.
4
+
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { ChatTopBar } from '../components/chat/ChatTopBar';
7
+ import { SessionList } from '../components/chat/SessionList';
8
+ import { ChatThread } from '../components/chat/ChatThread';
9
+ import { FloatingComposer } from '../components/chat/FloatingComposer';
10
+ import { InfoPanel } from '../components/chat/InfoPanel';
11
+ import { useChat } from '../components/chat/useChat';
12
+ import { useSlashCommands } from '../components/chat/useSlashCommands';
13
+ import { useToast } from '../components/Toast';
14
+ import { useModal } from '../components/Modal';
15
+ import { Button } from '../components/Button';
16
+ import type { Snapshot, Settings } from '../lib/types';
17
+
18
+ interface Props {
19
+ snapshot: Snapshot;
20
+ settings: Settings;
21
+ setActiveTab?: (id: string) => void;
22
+ refreshSnapshot?: () => Promise<void>;
23
+ initialTaskId?: string | null;
24
+ onClearTaskId?: () => void;
25
+ }
26
+
27
+ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearTaskId }: Props) {
28
+ const toast = useToast();
29
+ const modal = useModal();
30
+
31
+ // ── Delegate to useChat: messages, sessions, send, polling ──────────────
32
+ const chat = useChat(snapshot, settings, initialTaskId ?? '');
33
+
34
+ // ── Composer state (local) ───────────────────────────────────────────────
35
+ const [text, setText] = useState('');
36
+ const [agent, setAgent] = useState(settings.defaultAgent || 'odin');
37
+ const [model, setModel] = useState(settings.defaultModel || '');
38
+ const [attachments, setAttachments] = useState<string[]>([]);
39
+ const [creating, setCreating] = useState(false);
40
+
41
+ const fileInputRef = useRef<HTMLInputElement>(null);
42
+ const inputRef = useRef<HTMLTextAreaElement>(null);
43
+
44
+ // ── Slash commands ───────────────────────────────────────────────────────
45
+ const { allCommands, suggestions, setQuery } = useSlashCommands(snapshot);
46
+
47
+ useEffect(() => { setQuery(text); }, [text, setQuery]);
48
+
49
+ // ── File attach ──────────────────────────────────────────────────────────
50
+ const onAttach = () => fileInputRef.current?.click();
51
+
52
+ const onFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ const files = e.target.files;
54
+ if (!files) return;
55
+ const next: string[] = [];
56
+ for (let i = 0; i < files.length; i++) next.push(files[i].name);
57
+ setAttachments((cur) => {
58
+ const newNames = next.filter((n) => !cur.includes(n));
59
+ return [...cur, ...newNames];
60
+ });
61
+ if (fileInputRef.current) fileInputRef.current.value = '';
62
+ };
63
+
64
+ // ── Send ─────────────────────────────────────────────────────────────────
65
+ const handleSend = () => {
66
+ const msg = text.trim();
67
+ if (!msg) return;
68
+ setText('');
69
+ setQuery('');
70
+ chat.onSend(msg, agent, model, attachments);
71
+ };
72
+
73
+ // ── Create session ───────────────────────────────────────────────────────
74
+ const handleCreateSession = async () => {
75
+ if (creating) return;
76
+ if (!snapshot.activeProject) {
77
+ toast.warning('Pick a project in Overview to scope chat sessions.', 4000);
78
+ return;
79
+ }
80
+ setCreating(true);
81
+ try {
82
+ const created = await fetch('/chat/sessions', { method: 'POST' }).then((r) => r.json());
83
+ chat.loadChat(created.id);
84
+ toast.success(`Session ${created.id} created.`);
85
+ } catch (err) {
86
+ toast.error(`Create failed: ${(err as Error).message}`);
87
+ } finally {
88
+ setCreating(false);
89
+ }
90
+ };
91
+
92
+ // ── Delete message (via modal) ───────────────────────────────────────────
93
+ const handleDelete = (idx: number) => {
94
+ modal.open({
95
+ title: 'Delete message?',
96
+ children: <p style={{ margin: 0 }}>This action cannot be undone.</p>,
97
+ footer: (
98
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
99
+ <Button variant="secondary" size="sm" onClick={() => modal.close()}>Cancel</Button>
100
+ <Button
101
+ variant="danger"
102
+ size="sm"
103
+ onClick={() => {
104
+ modal.close();
105
+ chat.deleteMessage(idx);
106
+ }}
107
+ >
108
+ Delete
109
+ </Button>
110
+ </div>
111
+ ),
112
+ });
113
+ };
114
+
115
+ // ── Pin / copy are handled directly via chat.* ──────────────────────────
116
+
117
+ return (
118
+ <div className="chat-shell">
119
+ <ChatTopBar
120
+ activeProject={snapshot.activeProject}
121
+ sessionCount={chat.sessions.length}
122
+ sessionsOpen={true}
123
+ infoOpen={true}
124
+ onToggleSessions={() => {}}
125
+ onToggleInfo={() => {}}
126
+ onOpenOverview={() => setActiveTab?.('overview')}
127
+ />
128
+ <div className="chat-body">
129
+ <aside className="chat-sessions">
130
+ <SessionList
131
+ sessions={chat.sessions}
132
+ opencodeSessions={chat.opencodeSessions}
133
+ activeSessionId={chat.sessionId}
134
+ activeProject={snapshot.activeProject}
135
+ onCreateSession={handleCreateSession}
136
+ onSelectSession={chat.loadChat}
137
+ creating={creating}
138
+ />
139
+ </aside>
140
+
141
+ <main className="chat-main">
142
+ <ChatThread
143
+ messages={chat.messages}
144
+ loading={chat.loading}
145
+ activeProject={snapshot.activeProject}
146
+ sessionId={chat.sessionId}
147
+ pinned={chat.pinned}
148
+ onPickSuggestion={(t) => setText(t)}
149
+ onCopy={(m) => chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])}
150
+ onDelete={handleDelete}
151
+ onTogglePin={chat.togglePin}
152
+ onRegenerate={chat.onRegenerate}
153
+ />
154
+ <FloatingComposer
155
+ agent={agent}
156
+ setAgent={setAgent}
157
+ model={model}
158
+ setModel={setModel}
159
+ text={text}
160
+ setText={setText}
161
+ sending={chat.sending}
162
+ onSend={handleSend}
163
+ attachments={attachments}
164
+ setAttachments={setAttachments}
165
+ suggestions={suggestions}
166
+ onPickSuggestion={(cmd) => setText(`${cmd.split(' ')[0]} `)}
167
+ agents={snapshot.agents || []}
168
+ onAttach={onAttach}
169
+ sessionsOpen={true}
170
+ infoOpen={true}
171
+ />
172
+ <input ref={fileInputRef} type="file" multiple style={{ display: 'none' }} onChange={onFiles} />
173
+ </main>
174
+
175
+ <aside className="chat-info">
176
+ <InfoPanel
177
+ sessionId={chat.sessionId}
178
+ messages={chat.messages}
179
+ pinned={chat.pinned}
180
+ agent={agent}
181
+ model={model}
182
+ agents={snapshot.agents || []}
183
+ mcps={snapshot.mcps || []}
184
+ allCommands={allCommands}
185
+ />
186
+ </aside>
187
+ </div>
188
+ </div>
189
+ );
190
+ }