@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,613 @@
1
+ /**
2
+ * GlyphRenderer.tsx — Renders a compiled glyph (from /api/artifacts/:slug/render)
3
+ * with comment pin overlay + right-click to add comments.
4
+ *
5
+ * Uses the agent-native.com "Visual Plans" workflow (v3.22.0):
6
+ * - Compiled glyph structure: { frontmatter, blocks: [{id, type, data, childrenMarkdown?}], errors }
7
+ * - Each block renders via the components from ./components
8
+ * - Comment pins from comments.json overlay on top, positioned absolutely
9
+ * - Right-click anywhere → context menu with "Add comment" option
10
+ * - Blocks are grouped into sections by id (overview, implementation, …)
11
+ *
12
+ * Layout (v3.22.0 — agent-native /visual-plan inspired, dark dashboard styling):
13
+ * .glyph-canvas ← outer, dotted background
14
+ * .glyph-toolbar-floating ← sticky top-right pill (Send to agent, share, …)
15
+ * .glyph-canvas-content ← centered blocks area
16
+ * .glyph-section ← section card with optional heading
17
+ * .glyph-section-heading ← "Overview" / "Implementation plan" / …
18
+ * .glyph-section-blocks ← block stack inside the section
19
+ *
20
+ * v3.22.0 — section grouping + floating toolbar rewrite (kept all prior pin/ctx-menu logic).
21
+ */
22
+
23
+ import React, { useEffect, useMemo, useState, useRef } from 'react';
24
+ import {
25
+ MapPin,
26
+ MessageSquare,
27
+ Send,
28
+ Share2,
29
+ Undo2,
30
+ Redo2,
31
+ MoreHorizontal,
32
+ Maximize2,
33
+ X,
34
+ } from 'lucide-react';
35
+ import { api } from '../../lib/api';
36
+ import { useToast } from '../../components/Toast';
37
+ import {
38
+ RichText,
39
+ Callout,
40
+ Checklist,
41
+ Table,
42
+ CodeTabs,
43
+ Decision,
44
+ OpenQuestions,
45
+ FileTree,
46
+ Diff,
47
+ Stat,
48
+ Workflow,
49
+ Mockup,
50
+ } from './components';
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Types — match the server compiler output
54
+ // ---------------------------------------------------------------------------
55
+
56
+ type Block =
57
+ | { id: string; type: 'RichText'; data: Record<string, unknown>; childrenMarkdown?: string }
58
+ | { id: string; type: 'Callout'; data: { tone?: 'info' | 'warn' | 'success' | 'danger' }; childrenMarkdown?: string }
59
+ | { id: string; type: 'Checklist'; data: { items: { id: string; label: string; checked: boolean }[] } }
60
+ | { id: string; type: 'Table'; data: { columns: string[]; rows: string[][] } }
61
+ | { id: string; type: 'CodeTabs'; data: { tabs: { id: string; label: string; language: string; code: string; caption?: string }[] } }
62
+ | { id: string; type: 'Decision'; data: { title?: string; question?: string; options: { id: string; label: string; detail: string; recommended?: boolean }[] } }
63
+ | { id: string; type: 'OpenQuestions'; data: { questions: { id: string; label: string; kind: 'choice' | 'text' | 'multi'; options?: string[] }[] } }
64
+ | { id: string; type: 'FileTree'; data: { title?: string; entries: { path: string; change: 'added' | 'modified' | 'removed' | 'renamed'; note?: string }[] } }
65
+ | { id: string; type: 'Diff'; data: { filename?: string; language?: string; mode?: 'split' | 'unified'; before: string; after: string } }
66
+ | { id: string; type: 'Stat'; data: { label: string; value: string | number; trend?: 'up' | 'down' | 'flat'; hint?: string } }
67
+ | { id: string; type: 'Workflow'; data: { steps: { id: string; label: string; type: 'task' | 'decision' | 'note' }[]; connections?: { from: string; to: string; label?: string }[] } }
68
+ | { id: string; type: 'Mockup'; data: { title?: string; x?: number; y?: number; w?: number; h?: number; html: string } }
69
+ | { id: string; type: 'Diagram'; data: { title?: string; dataHtml: string; dataCss?: string } };
70
+
71
+ interface CompiledGlyph {
72
+ slug: string;
73
+ frontmatter: Record<string, unknown>;
74
+ blocks: Block[];
75
+ errors: string[];
76
+ compiledAt: string;
77
+ }
78
+
79
+ interface CommentPin {
80
+ id: string;
81
+ elementId?: string | null;
82
+ x: number;
83
+ y: number;
84
+ text: string;
85
+ author?: string;
86
+ created?: string;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Section grouping — block ids → section headings
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const SECTION_MAP: Record<string, string> = {
94
+ overview: 'Overview',
95
+ implementation: 'Implementation plan',
96
+ 'implementation-plan': 'Implementation plan',
97
+ questions: 'Open questions',
98
+ 'open-questions': 'Open questions',
99
+ 'open-questions-for-you': 'Open questions',
100
+ comments: 'Comments',
101
+ handoff: 'Handoff',
102
+ };
103
+
104
+ interface Section {
105
+ heading: string | null;
106
+ blocks: Block[];
107
+ }
108
+
109
+ function groupBlocksIntoSections(blocks: Block[]): Section[] {
110
+ const sections: Section[] = [];
111
+ let current: Section = { heading: null, blocks: [] };
112
+
113
+ for (const b of blocks) {
114
+ const heading = SECTION_MAP[b.id] ?? null;
115
+ if (heading) {
116
+ // Push any unheading prelude into a no-heading section so we don't lose it
117
+ if (current.blocks.length > 0 || current.heading !== null) {
118
+ sections.push(current);
119
+ }
120
+ current = { heading, blocks: [b] };
121
+ } else {
122
+ current.blocks.push(b);
123
+ }
124
+ }
125
+ if (current.blocks.length > 0 || current.heading !== null) {
126
+ sections.push(current);
127
+ }
128
+ return sections;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Props
133
+ // ---------------------------------------------------------------------------
134
+
135
+ interface Props {
136
+ slug: string;
137
+ onClose?: () => void;
138
+ /** Called after a comment is added so the parent can refresh. */
139
+ onCommentAdded?: () => void;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Component
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export function GlyphRenderer({ slug, onClose, onCommentAdded }: Props) {
147
+ const toast = useToast();
148
+ const [compiled, setCompiled] = useState<CompiledGlyph | null>(null);
149
+ const [comments, setComments] = useState<CommentPin[]>([]);
150
+ const [loading, setLoading] = useState(true);
151
+ const [error, setError] = useState<string | null>(null);
152
+ /** Error caught during the loading phase — shown inline so the user knows why. */
153
+ const [loadError, setLoadError] = useState<string | null>(null);
154
+ const [submitting, setSubmitting] = useState(false);
155
+ const [fullscreen, setFullscreen] = useState(false);
156
+
157
+ // Context-menu state
158
+ const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; worldX: number; worldY: number } | null>(null);
159
+ const [addingComment, setAddingComment] = useState<{ worldX: number; worldY: number } | null>(null);
160
+ const [commentDraft, setCommentDraft] = useState('');
161
+
162
+ // Active pin (expanded thread view)
163
+ const [activePin, setActivePin] = useState<string | null>(null);
164
+
165
+ const canvasRef = useRef<HTMLDivElement>(null);
166
+
167
+ // Section grouping — memoized so we don't re-walk the block array on every render
168
+ const sections = useMemo(
169
+ () => (compiled ? groupBlocksIntoSections(compiled.blocks) : []),
170
+ [compiled],
171
+ );
172
+
173
+ // Load compiled glyph + comments
174
+ useEffect(() => {
175
+ let cancelled = false;
176
+ const TIMEOUT_MS = 5000;
177
+
178
+ (async () => {
179
+ setLoading(true);
180
+ setError(null);
181
+ try {
182
+ // Race the API calls against a 5-second timeout so the user
183
+ // sees an error rather than a perpetual spinner if the server
184
+ // hangs or the network drops.
185
+ const timeoutPromise = new Promise((_, reject) =>
186
+ setTimeout(() => reject(new Error('Request timed out after 5s')), TIMEOUT_MS)
187
+ );
188
+
189
+ const [renderData, artifactData] = await Promise.race([
190
+ Promise.all([
191
+ api.get<CompiledGlyph>(`/artifacts/${encodeURIComponent(slug)}/render`),
192
+ api.get<{ comments?: CommentPin[]; commentsJson?: { comments?: CommentPin[] } }>(
193
+ `/artifacts/${encodeURIComponent(slug)}`
194
+ ),
195
+ ]),
196
+ timeoutPromise,
197
+ ]) as [CompiledGlyph, { comments?: CommentPin[]; commentsJson?: { comments?: CommentPin[] } }];
198
+
199
+ if (cancelled) return;
200
+ setCompiled(renderData);
201
+ const fromFile = (artifactData as any)?.comments ?? [];
202
+ setComments(Array.isArray(fromFile) ? fromFile : []);
203
+ } catch (err) {
204
+ if (!cancelled) {
205
+ const msg = (err as Error).message;
206
+ setError(msg);
207
+ setLoadError(msg);
208
+ }
209
+ } finally {
210
+ if (!cancelled) setLoading(false);
211
+ }
212
+ })();
213
+ return () => {
214
+ cancelled = true;
215
+ };
216
+ }, [slug]);
217
+
218
+ // Right-click context menu
219
+ function onContainerContextMenu(e: React.MouseEvent) {
220
+ if (!canvasRef.current) return;
221
+ e.preventDefault();
222
+ const rect = canvasRef.current.getBoundingClientRect();
223
+ setCtxMenu({
224
+ x: e.clientX,
225
+ y: e.clientY,
226
+ worldX: e.clientX - rect.left,
227
+ worldY: e.clientY - rect.top,
228
+ });
229
+ }
230
+
231
+ async function submitComment() {
232
+ if (!addingComment || !commentDraft.trim()) return;
233
+ try {
234
+ const created = await api.post<{ id?: string }>(
235
+ `/artifacts/${encodeURIComponent(slug)}/comments`,
236
+ {
237
+ x: addingComment.worldX,
238
+ y: addingComment.worldY,
239
+ text: commentDraft.trim(),
240
+ author: 'drb0rk',
241
+ }
242
+ );
243
+ setComments((prev) => [
244
+ ...prev,
245
+ {
246
+ id: created.id ?? `cmt_${Date.now()}`,
247
+ x: addingComment.worldX,
248
+ y: addingComment.worldY,
249
+ text: commentDraft.trim(),
250
+ author: 'drb0rk',
251
+ created: new Date().toISOString(),
252
+ },
253
+ ]);
254
+ setAddingComment(null);
255
+ setCommentDraft('');
256
+ setCtxMenu(null);
257
+ onCommentAdded?.();
258
+ toast.success('Comment added');
259
+ } catch (err) {
260
+ toast.error(`Failed to add comment: ${(err as Error).message}`);
261
+ }
262
+ }
263
+
264
+ // "Send to agent" — collects free-placed comments and any answers to
265
+ // OpenQuestions, writes `feedback.md` on disk, and marks the artifact
266
+ // `status: review` so the agent picks it up.
267
+ async function submitFeedback() {
268
+ if (submitting) return;
269
+ setSubmitting(true);
270
+ try {
271
+ const result = await api.post<{ ok: boolean; commentCount?: number; questionCount?: number }>(
272
+ `/artifacts/${encodeURIComponent(slug)}/submit`,
273
+ { answers: [], submitter: 'drb0rk' },
274
+ );
275
+ if (result?.ok) {
276
+ toast.success('Sent to agent — feedback.md written, status=review');
277
+ onCommentAdded?.();
278
+ } else {
279
+ toast.error('Submit failed: server did not return ok=true');
280
+ }
281
+ } catch (err) {
282
+ toast.error(`Submit failed: ${(err as Error).message}`);
283
+ } finally {
284
+ setSubmitting(false);
285
+ }
286
+ }
287
+
288
+ function toggleFullscreen() {
289
+ setFullscreen((v) => !v);
290
+ }
291
+
292
+ // Render a single block
293
+ const renderBlock = (b: Block) => {
294
+ const id = b.id;
295
+ const data = (b.data ?? {}) as Record<string, unknown>;
296
+ switch (b.type) {
297
+ case 'RichText':
298
+ return <RichText key={id} id={id}>{b.childrenMarkdown ?? ''}</RichText>;
299
+ case 'Callout':
300
+ return (
301
+ <Callout key={id} id={id} tone={(data.tone as 'info' | 'warn' | 'success' | 'danger' | undefined) ?? 'info'}>
302
+ {b.childrenMarkdown ?? ''}
303
+ </Callout>
304
+ );
305
+ case 'Checklist':
306
+ return (
307
+ <Checklist key={id} id={id} items={(data.items as any) ?? []} />
308
+ );
309
+ case 'Table':
310
+ return (
311
+ <Table key={id} id={id} columns={(data.columns as string[]) ?? []} rows={(data.rows as string[][]) ?? []} />
312
+ );
313
+ case 'CodeTabs':
314
+ return (
315
+ <CodeTabs key={id} id={id} tabs={(data.tabs as any) ?? []} />
316
+ );
317
+ case 'Decision':
318
+ return (
319
+ <Decision
320
+ key={id}
321
+ id={id}
322
+ title={data.title as string | undefined}
323
+ question={data.question as string | undefined}
324
+ options={(data.options as any) ?? []}
325
+ />
326
+ );
327
+ case 'OpenQuestions':
328
+ return (
329
+ <OpenQuestions
330
+ key={id}
331
+ id={id}
332
+ questions={(data.questions as any) ?? []}
333
+ />
334
+ );
335
+ case 'FileTree':
336
+ return (
337
+ <FileTree
338
+ key={id}
339
+ id={id}
340
+ title={data.title as string | undefined}
341
+ entries={(data.entries as any) ?? []}
342
+ />
343
+ );
344
+ case 'Diff':
345
+ return (
346
+ <Diff
347
+ key={id}
348
+ id={id}
349
+ filename={data.filename as string | undefined}
350
+ language={data.language as string | undefined}
351
+ mode={(data.mode as 'split' | 'unified' | undefined) ?? 'unified'}
352
+ before={(data.before as string) ?? ''}
353
+ after={(data.after as string) ?? ''}
354
+ />
355
+ );
356
+ case 'Stat':
357
+ return (
358
+ <Stat
359
+ key={id}
360
+ id={id}
361
+ label={(data.label as string) ?? ''}
362
+ value={data.value as string | number}
363
+ trend={data.trend as 'up' | 'down' | 'flat' | undefined}
364
+ hint={data.hint as string | undefined}
365
+ />
366
+ );
367
+ case 'Workflow':
368
+ return (
369
+ <Workflow
370
+ key={id}
371
+ id={id}
372
+ steps={(data.steps as any) ?? []}
373
+ connections={data.connections as any}
374
+ />
375
+ );
376
+ case 'Mockup':
377
+ return (
378
+ <Mockup
379
+ key={id}
380
+ id={id}
381
+ title={data.title as string | undefined}
382
+ x={data.x as number | undefined}
383
+ y={data.y as number | undefined}
384
+ w={data.w as number | undefined}
385
+ h={data.h as number | undefined}
386
+ html={(data.html as string) ?? ''}
387
+ />
388
+ );
389
+ case 'Diagram':
390
+ return (
391
+ <div key={id} id={id} className="glyph-block-placeholder">
392
+ <em>[Diagram] {id}</em>
393
+ </div>
394
+ );
395
+ }
396
+ };
397
+
398
+ if (loading) {
399
+ return (
400
+ <div className="glyph-renderer glyph-renderer--loading">
401
+ {loadError ? (
402
+ <>
403
+ <strong>Failed to load glyph.</strong>
404
+ <pre>{loadError}</pre>
405
+ </>
406
+ ) : (
407
+ <>
408
+ <Spinner /> Loading glyph…
409
+ </>
410
+ )}
411
+ </div>
412
+ );
413
+ }
414
+
415
+ if (error || !compiled) {
416
+ return (
417
+ <div className="glyph-renderer glyph-renderer--error">
418
+ <strong>Failed to load glyph.</strong>
419
+ <pre>{error ?? 'unknown error'}</pre>
420
+ </div>
421
+ );
422
+ }
423
+
424
+ const title = (compiled.frontmatter?.title as string) ?? slug;
425
+ const status = (compiled.frontmatter?.status as string) ?? 'draft';
426
+ const commentCount = comments.length;
427
+
428
+ return (
429
+ <div
430
+ className={`glyph-canvas ${fullscreen ? 'glyph-canvas--fullscreen' : ''}`}
431
+ ref={canvasRef}
432
+ onContextMenu={onContainerContextMenu}
433
+ >
434
+ {/* Floating toolbar (sticky top-right pill) */}
435
+ <div className="glyph-toolbar-floating">
436
+ <button
437
+ className="glyph-btn glyph-btn--primary glyph-btn--send"
438
+ onClick={submitFeedback}
439
+ disabled={submitting}
440
+ title="Send comments + question answers to the agent — writes feedback.md, status=review"
441
+ >
442
+ <Send size={14} />
443
+ <span>Send to agent</span>
444
+ <span className="glyph-btn-badge">{commentCount}</span>
445
+ </button>
446
+
447
+ <span className="glyph-toolbar-divider" />
448
+
449
+ <button
450
+ className="glyph-icon-btn"
451
+ title="Share"
452
+ onClick={() => {
453
+ if (typeof window !== 'undefined' && navigator.clipboard) {
454
+ navigator.clipboard.writeText(window.location.href).catch(() => {});
455
+ toast.success('Link copied');
456
+ }
457
+ }}
458
+ >
459
+ <Share2 size={15} />
460
+ </button>
461
+ <button className="glyph-icon-btn" title="Undo" disabled>
462
+ <Undo2 size={15} />
463
+ </button>
464
+ <button className="glyph-icon-btn" title="Redo" disabled>
465
+ <Redo2 size={15} />
466
+ </button>
467
+ <button
468
+ className="glyph-icon-btn"
469
+ title={fullscreen ? 'Exit fullscreen' : 'Fullscreen'}
470
+ onClick={toggleFullscreen}
471
+ >
472
+ <Maximize2 size={15} />
473
+ </button>
474
+ <button className="glyph-icon-btn" title="More">
475
+ <MoreHorizontal size={15} />
476
+ </button>
477
+ <span className="glyph-toolbar-divider" />
478
+ <button
479
+ className="glyph-icon-btn"
480
+ title="Comment count"
481
+ aria-label="Comment count"
482
+ >
483
+ <MessageSquare size={15} />
484
+ <span className="glyph-icon-btn-count">{commentCount}</span>
485
+ </button>
486
+ {onClose && (
487
+ <button
488
+ className="glyph-icon-btn glyph-icon-btn--close"
489
+ title="Close"
490
+ onClick={onClose}
491
+ aria-label="Close"
492
+ >
493
+ <X size={15} />
494
+ </button>
495
+ )}
496
+ </div>
497
+
498
+ {/* Canvas content — centered column of section cards */}
499
+ <div className="glyph-canvas-content">
500
+ {/* Title strip (inside canvas, above sections) */}
501
+ <header className="glyph-canvas-title">
502
+ <h1 className="glyph-title">{title}</h1>
503
+ <div className="glyph-meta">
504
+ <span className={`glyph-status glyph-status--${status}`}>{status}</span>
505
+ <span className="glyph-slug">{slug}</span>
506
+ </div>
507
+ </header>
508
+
509
+ {/* Compiler warnings */}
510
+ {compiled.errors?.length > 0 && (
511
+ <div className="glyph-errors">
512
+ <strong>Compiler warnings:</strong>
513
+ <ul>
514
+ {compiled.errors.map((e, i) => (
515
+ <li key={i}>{e}</li>
516
+ ))}
517
+ </ul>
518
+ </div>
519
+ )}
520
+
521
+ {/* Sections */}
522
+ <div className="glyph-sections">
523
+ {sections.map((section, i) => (
524
+ <section
525
+ key={`sec-${i}-${section.heading ?? 'ungrouped'}`}
526
+ className={`glyph-section ${section.heading ? 'glyph-section--headed' : 'glyph-section--plain'}`}
527
+ >
528
+ {section.heading && (
529
+ <h2 className="glyph-section-heading">{section.heading}</h2>
530
+ )}
531
+ <div className="glyph-section-blocks">
532
+ {section.blocks.map(renderBlock)}
533
+ </div>
534
+ </section>
535
+ ))}
536
+ </div>
537
+ </div>
538
+
539
+ {/* Comment pin overlay */}
540
+ {comments.map((c) => (
541
+ <button
542
+ key={c.id}
543
+ className={`glyph-pin ${activePin === c.id ? 'glyph-pin--active' : ''}`}
544
+ style={{ left: c.x, top: c.y }}
545
+ onClick={(e) => {
546
+ e.stopPropagation();
547
+ setActivePin(activePin === c.id ? null : c.id);
548
+ }}
549
+ title={c.text}
550
+ >
551
+ <MapPin size={14} />
552
+ {activePin === c.id && (
553
+ <div className="glyph-pin-thread">
554
+ <div className="glyph-pin-text">{c.text}</div>
555
+ <div className="glyph-pin-meta">
556
+ {c.author ?? 'anonymous'} · {c.created ? new Date(c.created).toLocaleString() : ''}
557
+ </div>
558
+ </div>
559
+ )}
560
+ </button>
561
+ ))}
562
+
563
+ {/* Context menu (right-click) */}
564
+ {ctxMenu && (
565
+ <div
566
+ className="glyph-ctx-menu"
567
+ style={{ left: ctxMenu.x, top: ctxMenu.y }}
568
+ onClick={(e) => e.stopPropagation()}
569
+ >
570
+ <button
571
+ className="glyph-ctx-item"
572
+ onClick={() => {
573
+ setAddingComment({ worldX: ctxMenu.worldX, worldY: ctxMenu.worldY });
574
+ setCtxMenu(null);
575
+ }}
576
+ >
577
+ <MapPin size={14} /> Add comment here
578
+ </button>
579
+ </div>
580
+ )}
581
+
582
+ {/* Add-comment modal */}
583
+ {addingComment && (
584
+ <div className="glyph-modal-overlay" onClick={() => setAddingComment(null)}>
585
+ <div className="glyph-modal" onClick={(e) => e.stopPropagation()}>
586
+ <h3>Add comment at ({Math.round(addingComment.worldX)}, {Math.round(addingComment.worldY)})</h3>
587
+ <textarea
588
+ autoFocus
589
+ value={commentDraft}
590
+ onChange={(e) => setCommentDraft(e.target.value)}
591
+ placeholder="What should the agent know about this area?"
592
+ rows={4}
593
+ className="glyph-modal-textarea"
594
+ />
595
+ <div className="glyph-modal-actions">
596
+ <button className="glyph-btn glyph-btn--ghost" onClick={() => setAddingComment(null)}>
597
+ Cancel
598
+ </button>
599
+ <button className="glyph-btn glyph-btn--primary" onClick={submitComment} disabled={!commentDraft.trim()}>
600
+ Add comment
601
+ </button>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ )}
606
+ </div>
607
+ );
608
+ }
609
+
610
+ // Tiny inline spinner (no extra dep)
611
+ function Spinner() {
612
+ return <span className="glyph-spinner" aria-label="loading">…</span>;
613
+ }