@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,1203 @@
1
+ // src/views/Activity.tsx — v3.5.4: timeline-based activity view (replaces the v3.5.2 graph+strip hybrid).
2
+ // Layout: CSS grid with 3 sibling columns — left event stream, center timeline canvas, right detail panel.
3
+ // X axis is time; Y axis is lanes (BG instances + tasks). Active tasks pulse; queued tasks are translucent
4
+ // dashed; done tasks are faded. A red "now" line updates every 1s and re-anchors when it nears the right edge.
5
+
6
+ import {
7
+ useCallback,
8
+ useEffect,
9
+ useLayoutEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
14
+ import {
15
+ Activity as ActivityIcon,
16
+ Bot,
17
+ CheckSquare,
18
+ Cpu,
19
+ RefreshCw,
20
+ Send,
21
+ X,
22
+ Plus,
23
+ Trash2,
24
+ MessageSquare,
25
+ History,
26
+ Pause,
27
+ Play,
28
+ PanelLeftClose,
29
+ PanelLeftOpen,
30
+ Layers,
31
+ Target,
32
+ FileText,
33
+ } from 'lucide-react';
34
+ import { Button } from '../components/Button';
35
+ import { Card, CardTitle } from '../components/Card';
36
+ import { EmptyState } from '../components/EmptyState';
37
+ import { Spinner } from '../components/Spinner';
38
+ import { useToast } from '../components/Toast';
39
+ import { useModal } from '../components/Modal';
40
+ import { api } from '../lib/api';
41
+ import { cn } from '../lib/utils';
42
+ import type { Settings, Snapshot, Task, Agent } from '../lib/types';
43
+ import { openArtifactViewer } from '../components/ArtifactViewer';
44
+
45
+ type Props = {
46
+ snapshot: Snapshot;
47
+ settings: Settings;
48
+ activeTab: string;
49
+ setActiveTab: (id: string) => void;
50
+ refreshSnapshot: () => Promise<void>;
51
+ };
52
+
53
+ type BgInstance = {
54
+ instanceId: string;
55
+ agent?: string;
56
+ status?: string;
57
+ startedAt?: number;
58
+ completedAt?: number;
59
+ lastEventAt?: number;
60
+ promptPreview?: string;
61
+ resultPreview?: string;
62
+ error?: string;
63
+ parentAgent?: string;
64
+ parentInstanceId?: string;
65
+ tmuxSession?: string;
66
+ tmuxActive?: boolean;
67
+ taskId?: string; // v3.6.2 — linked task for artifact lookup
68
+ _bgDir?: string;
69
+ };
70
+
71
+ type ActivityEvent = {
72
+ ts: string;
73
+ kind: string;
74
+ nodeId?: string;
75
+ text?: string;
76
+ author?: string;
77
+ taskId?: string;
78
+ [k: string]: unknown;
79
+ };
80
+
81
+ // ─── Time / layout constants ──────────────────────────────────────────
82
+
83
+ const ZOOM_RANGES = {
84
+ '1m': 60 * 1000,
85
+ '5m': 5 * 60 * 1000,
86
+ '30m': 30 * 60 * 1000,
87
+ '1h': 60 * 60 * 1000,
88
+ } as const;
89
+ type ZoomKey = keyof typeof ZOOM_RANGES;
90
+
91
+ const LANE_HEIGHT = 56;
92
+ const TIME_AXIS_HEIGHT = 32;
93
+ const EVENTS_STRIP_HEIGHT = 22;
94
+ const MIN_BAR_WIDTH = 4;
95
+ const REANCHOR_RATIO = 0.9; // when now_x exceeds this fraction of canvas width, re-anchor viewStart
96
+
97
+ // ─── Status → color ───────────────────────────────────────────────────
98
+
99
+ const STATUS_COLORS: Record<string, string> = {
100
+ working: 'var(--success)',
101
+ running: 'var(--success)',
102
+ doing: 'var(--info)',
103
+ queued: 'var(--info)',
104
+ done: 'var(--success)',
105
+ success: 'var(--success)',
106
+ blocked: 'var(--warning)',
107
+ error: 'var(--error)',
108
+ failed: 'var(--error)',
109
+ stuck: 'var(--warning)',
110
+ killed: 'var(--error)',
111
+ idle: 'var(--text-dim)',
112
+ pending: 'var(--info)',
113
+ timed_out: 'var(--warning)',
114
+ };
115
+
116
+ function statusColor(s: string | null | undefined): string {
117
+ if (!s) return STATUS_COLORS.idle;
118
+ return STATUS_COLORS[s] || STATUS_COLORS.idle;
119
+ }
120
+
121
+ function shortLabel(s: string | null | undefined, n = 24): string {
122
+ if (!s) return '';
123
+ if (s.length <= n) return s;
124
+ return s.slice(0, n - 1) + '…';
125
+ }
126
+
127
+ // ─── Time helpers ─────────────────────────────────────────────────────
128
+
129
+ function parseTs(ts: string | number | null | undefined): number {
130
+ if (ts == null) return 0;
131
+ if (typeof ts === 'number') return ts;
132
+ const t = new Date(ts).getTime();
133
+ return Number.isNaN(t) ? 0 : t;
134
+ }
135
+
136
+ function taskStartMs(t: Task): number {
137
+ // v3.5.4 — prefer _timerStart (set when work begins), fall back to createdAt.
138
+ const ts = (t as { _timerStart?: number })._timerStart;
139
+ if (typeof ts === 'number' && ts > 0) return ts;
140
+ return parseTs(t.createdAt) || Date.now();
141
+ }
142
+
143
+ function taskEndMs(t: Task, now: number): number {
144
+ if (t.status === 'done' || t.status === 'archived' || t.status === 'failed' || t.status === 'killed') {
145
+ return parseTs(t.completedAt) || parseTs(t.updatedAt) || now;
146
+ }
147
+ // Active tasks: pin to now.
148
+ return now;
149
+ }
150
+
151
+ function bgStartMs(b: BgInstance): number {
152
+ return typeof b.startedAt === 'number' && b.startedAt > 0 ? b.startedAt : Date.now();
153
+ }
154
+
155
+ function bgEndMs(b: BgInstance, now: number): number {
156
+ if (
157
+ b.status === 'done'
158
+ || b.status === 'success'
159
+ || b.status === 'killed'
160
+ || b.status === 'failed'
161
+ || b.status === 'error'
162
+ || b.status === 'timed_out'
163
+ ) {
164
+ return b.completedAt || b.lastEventAt || now;
165
+ }
166
+ return now;
167
+ }
168
+
169
+ function chooseTickStep(rangeMs: number): number {
170
+ if (rangeMs <= 60_000) return 10_000;
171
+ if (rangeMs <= 300_000) return 30_000;
172
+ if (rangeMs <= 1_800_000) return 300_000;
173
+ return 600_000;
174
+ }
175
+
176
+ function formatTickLabel(t: number, rangeMs: number): string {
177
+ const d = new Date(t);
178
+ if (rangeMs <= 300_000) {
179
+ return d.toLocaleTimeString('en-GB', { hour12: false });
180
+ }
181
+ return d.toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' });
182
+ }
183
+
184
+ // ─── Lane + tick types ────────────────────────────────────────────────
185
+
186
+ type Lane =
187
+ | { kind: 'bg'; id: string; index: number; label: string; sub: string; start: number; end: number; data: BgInstance }
188
+ | { kind: 'task'; id: string; index: number; label: string; sub: string; start: number; end: number; data: Task }
189
+ | { kind: 'events'; id: string; index: number; label: string; sub: string; start: number; end: number };
190
+
191
+ type SelectedItem = {
192
+ id: string;
193
+ kind: 'task' | 'bg' | 'agent';
194
+ label: string;
195
+ status: string;
196
+ data: Task | BgInstance | Agent;
197
+ };
198
+
199
+ type EventMarker = {
200
+ id: string;
201
+ x: number;
202
+ y: number;
203
+ kind: string;
204
+ ts: number;
205
+ text: string;
206
+ author?: string;
207
+ };
208
+
209
+ type TaskBar = {
210
+ id: string;
211
+ laneIndex: number;
212
+ x: number;
213
+ y: number;
214
+ w: number;
215
+ h: number;
216
+ label: string;
217
+ statusClass: 'doing' | 'done' | 'queued' | 'blocked' | 'failed';
218
+ selected: boolean;
219
+ data: Task | BgInstance;
220
+ kind: 'task' | 'bg';
221
+ start: number;
222
+ end: number;
223
+ };
224
+
225
+ // ─── Component ────────────────────────────────────────────────────────
226
+
227
+ export function Activity({ snapshot, refreshSnapshot }: Props) {
228
+ const toast = useToast();
229
+ const canvasWrapRef = useRef<HTMLDivElement | null>(null);
230
+
231
+ // View state
232
+ const [mode, setMode] = useState<'live' | 'pause'>('live');
233
+ const [zoom, setZoom] = useState<ZoomKey>('5m');
234
+ const [streamOpen, setStreamOpen] = useState(() => {
235
+ if (typeof window === 'undefined') return true;
236
+ return window.innerWidth > 900;
237
+ });
238
+
239
+ // Data
240
+ const [bgInstances, setBgInstances] = useState<BgInstance[]>([]);
241
+ const [events, setEvents] = useState<ActivityEvent[]>([]);
242
+ const [loading, setLoading] = useState(true);
243
+ const [refreshTick, setRefreshTick] = useState(0);
244
+
245
+ // Time
246
+ const [nowTick, setNowTick] = useState<number>(Date.now());
247
+ const [viewStart, setViewStart] = useState<number>(() => Date.now() - ZOOM_RANGES['5m'] * 0.1);
248
+
249
+ // Canvas size (observed)
250
+ const [canvasSize, setCanvasSize] = useState<{ width: number; height: number }>({ width: 800, height: 400 });
251
+
252
+ // Selection / detail
253
+ const [selectedItem, setSelectedItem] = useState<SelectedItem | null>(null);
254
+
255
+ // Detail panel local state
256
+ const [commentText, setCommentText] = useState('');
257
+ const [comments, setComments] = useState<ActivityEvent[]>([]);
258
+ const [taskTitle, setTaskTitle] = useState('');
259
+ const [taskPriority, setTaskPriority] = useState('normal');
260
+ const [bgMessage, setBgMessage] = useState('');
261
+ const [bgOutput, setBgOutput] = useState('');
262
+ const [creatingTask, setCreatingTask] = useState(false);
263
+ const [postingComment, setPostingComment] = useState(false);
264
+ // v3.6.2 — artifact IDs fetched from the task linked to the selected bg instance.
265
+ const [bgArtifactIds, setBgArtifactIds] = useState<string[]>([]);
266
+
267
+ const modal = useModal();
268
+
269
+ const agents = snapshot.agents || [];
270
+ const tasks = snapshot.tasks || [];
271
+ const rangeMs = ZOOM_RANGES[zoom];
272
+ const rangeStart = viewStart;
273
+ const rangeEnd = viewStart + rangeMs;
274
+
275
+ // ─── Data polling ───────────────────────────────────────────────────
276
+ const reloadAll = useCallback(async () => {
277
+ try {
278
+ const [bgRes, evRes] = await Promise.all([
279
+ api.get<{ instances: BgInstance[] }>('/background').catch(() => ({ instances: [] })),
280
+ api.get<{ events: ActivityEvent[] }>('/activity?limit=200').catch(() => ({ events: [] })),
281
+ ]);
282
+ setBgInstances(bgRes.instances || []);
283
+ setEvents(evRes.events || []);
284
+ } catch (err) {
285
+ console.warn('activity reload failed:', err);
286
+ } finally {
287
+ setLoading(false);
288
+ }
289
+ }, []);
290
+
291
+ useEffect(() => {
292
+ reloadAll();
293
+ const id = setInterval(reloadAll, 3000);
294
+ return () => clearInterval(id);
295
+ }, [reloadAll]);
296
+
297
+ // Trigger a re-fetch when the snapshot's task/agent counts change.
298
+ useEffect(() => {
299
+ setRefreshTick((t) => t + 1);
300
+ // eslint-disable-next-line react-hooks/exhaustive-deps
301
+ }, [tasks.length, agents.length]);
302
+
303
+ // ─── Now tick (1s) — paused when mode === 'pause' ───────────────────
304
+ useEffect(() => {
305
+ if (mode === 'pause') return;
306
+ setNowTick(Date.now());
307
+ const id = setInterval(() => setNowTick(Date.now()), 1000);
308
+ return () => clearInterval(id);
309
+ }, [mode, refreshTick]);
310
+
311
+ // ─── Re-anchor viewStart when the now line nears the right edge ────
312
+ useEffect(() => {
313
+ if (mode === 'pause') return;
314
+ const nowRatio = (nowTick - viewStart) / rangeMs;
315
+ if (nowRatio > REANCHOR_RATIO) {
316
+ setViewStart(nowTick - rangeMs * 0.1);
317
+ }
318
+ }, [nowTick, viewStart, rangeMs, mode]);
319
+
320
+ // On zoom change, reset viewStart so now sits at 10% from the left.
321
+ useEffect(() => {
322
+ setViewStart(Date.now() - rangeMs * 0.1);
323
+ // eslint-disable-next-line react-hooks/exhaustive-deps
324
+ }, [zoom]);
325
+
326
+ // On resume from pause, re-anchor so the now line lands at 10%.
327
+ useEffect(() => {
328
+ if (mode === 'live') {
329
+ setViewStart(Date.now() - rangeMs * 0.1);
330
+ }
331
+ // eslint-disable-next-line react-hooks/exhaustive-deps
332
+ }, [mode]);
333
+
334
+ // ─── Canvas size observer ───────────────────────────────────────────
335
+ useLayoutEffect(() => {
336
+ const el = canvasWrapRef.current;
337
+ if (!el) return;
338
+ const ro = new ResizeObserver((entries) => {
339
+ for (const entry of entries) {
340
+ const { width, height } = entry.contentRect;
341
+ setCanvasSize({
342
+ width: Math.max(200, Math.floor(width)),
343
+ height: Math.max(120, Math.floor(height)),
344
+ });
345
+ }
346
+ });
347
+ ro.observe(el);
348
+ return () => ro.disconnect();
349
+ }, []);
350
+
351
+ // ─── Lane allocation ────────────────────────────────────────────────
352
+ const lanes: Lane[] = useMemo(() => {
353
+ const out: Lane[] = [];
354
+ let idx = 0;
355
+
356
+ // BG instances — one lane each, at the top.
357
+ for (const bg of bgInstances) {
358
+ const start = bgStartMs(bg);
359
+ const end = bgEndMs(bg, nowTick);
360
+ if (end < rangeStart || start > rangeEnd) continue;
361
+ out.push({
362
+ kind: 'bg',
363
+ id: `bg:${bg.instanceId}`,
364
+ index: idx++,
365
+ label: `BG ${shortLabel(bg.promptPreview, 20) || bg.instanceId.slice(0, 10)}`,
366
+ sub: bg.status || 'pending',
367
+ start,
368
+ end,
369
+ data: bg,
370
+ });
371
+ }
372
+
373
+ // Tasks — greedy lane assignment (fill first lane that has no overlap).
374
+ const sorted = [...tasks].sort((a, b) => taskStartMs(a) - taskStartMs(b));
375
+ const taskLaneEnds: number[] = []; // end-time of last task placed in each lane
376
+ for (const t of sorted) {
377
+ const s = taskStartMs(t);
378
+ const e = taskEndMs(t, nowTick);
379
+ if (e < rangeStart || s > rangeEnd) continue;
380
+ let assigned = -1;
381
+ for (let i = 0; i < taskLaneEnds.length; i++) {
382
+ if (taskLaneEnds[i] <= s) {
383
+ taskLaneEnds[i] = e;
384
+ assigned = i;
385
+ break;
386
+ }
387
+ }
388
+ if (assigned === -1) {
389
+ taskLaneEnds.push(e);
390
+ assigned = taskLaneEnds.length - 1;
391
+ }
392
+ out.push({
393
+ kind: 'task',
394
+ id: `task:${t.id}`,
395
+ index: idx + assigned,
396
+ label: shortLabel(t.title, 32),
397
+ sub: t.status,
398
+ start: s,
399
+ end: e,
400
+ data: t,
401
+ });
402
+ }
403
+
404
+ return out;
405
+ }, [bgInstances, tasks, nowTick, rangeStart, rangeEnd]);
406
+
407
+ const laneById = useMemo(() => {
408
+ const m = new Map<string, Lane>();
409
+ for (const l of lanes) m.set(l.id, l);
410
+ return m;
411
+ }, [lanes]);
412
+
413
+ // ─── Time → X coordinate ────────────────────────────────────────────
414
+ const timeToX = useCallback(
415
+ (t: number): number => {
416
+ if (rangeMs <= 0) return 0;
417
+ return ((t - rangeStart) / rangeMs) * canvasSize.width;
418
+ },
419
+ [rangeStart, rangeMs, canvasSize.width],
420
+ );
421
+
422
+ // ─── Tick marks for the time axis ───────────────────────────────────
423
+ const ticks = useMemo(() => {
424
+ const step = chooseTickStep(rangeMs);
425
+ const result: { t: number; x: number; label: string }[] = [];
426
+ // Align to step boundary.
427
+ const startAligned = Math.floor(rangeStart / step) * step;
428
+ for (let t = startAligned; t <= rangeEnd + step; t += step) {
429
+ if (t < rangeStart - step) continue;
430
+ if (t > rangeEnd) break;
431
+ const x = timeToX(t);
432
+ if (x < -40 || x > canvasSize.width + 40) continue;
433
+ result.push({ t, x, label: formatTickLabel(t, rangeMs) });
434
+ }
435
+ return result;
436
+ }, [rangeStart, rangeEnd, rangeMs, canvasSize.width, timeToX]);
437
+
438
+ // ─── Build task/bg bars ─────────────────────────────────────────────
439
+ const bars: TaskBar[] = useMemo(() => {
440
+ const out: TaskBar[] = [];
441
+ for (const lane of lanes) {
442
+ if (lane.kind === 'events') continue;
443
+ const visibleStart = Math.max(lane.start, rangeStart);
444
+ const visibleEnd = Math.min(lane.end, rangeEnd);
445
+ if (visibleEnd < rangeStart || visibleStart > rangeEnd) continue;
446
+ const x = timeToX(visibleStart);
447
+ const xEnd = timeToX(visibleEnd);
448
+ const w = Math.max(MIN_BAR_WIDTH, xEnd - x);
449
+ const y = TIME_AXIS_HEIGHT + EVENTS_STRIP_HEIGHT + lane.index * LANE_HEIGHT + 8;
450
+ const h = LANE_HEIGHT - 16;
451
+
452
+ let statusClass: TaskBar['statusClass'] = 'doing';
453
+ if (lane.kind === 'bg') {
454
+ const s = lane.data.status || 'pending';
455
+ if (s === 'done' || s === 'success') statusClass = 'done';
456
+ else if (s === 'failed' || s === 'killed' || s === 'error') statusClass = 'failed';
457
+ else if (s === 'queued' || s === 'pending') statusClass = 'queued';
458
+ else if (s === 'blocked' || s === 'stuck') statusClass = 'blocked';
459
+ else statusClass = 'doing';
460
+ } else {
461
+ const s = lane.data.status;
462
+ if (s === 'done' || s === 'archived') statusClass = 'done';
463
+ else if (s === 'failed' || s === 'killed') statusClass = 'failed';
464
+ else if (s === 'queued') statusClass = 'queued';
465
+ else if (s === 'blocked') statusClass = 'blocked';
466
+ else statusClass = 'doing';
467
+ }
468
+
469
+ const selected = selectedItem?.id === lane.id;
470
+
471
+ out.push({
472
+ id: lane.id,
473
+ laneIndex: lane.index,
474
+ x,
475
+ y,
476
+ w,
477
+ h,
478
+ label: lane.label,
479
+ statusClass,
480
+ selected,
481
+ data: lane.data,
482
+ kind: lane.kind,
483
+ start: lane.start,
484
+ end: lane.end,
485
+ });
486
+ }
487
+ return out;
488
+ }, [lanes, timeToX, selectedItem, rangeStart, rangeEnd]);
489
+
490
+ // ─── Build event markers (placed on the lane of their related entity) ─
491
+ const eventMarkers: EventMarker[] = useMemo(() => {
492
+ const out: EventMarker[] = [];
493
+ for (const ev of events) {
494
+ const ts = parseTs(ev.ts);
495
+ if (ts < rangeStart - 5000 || ts > rangeEnd + 5000) continue;
496
+ const x = timeToX(ts);
497
+
498
+ // Try to find a related lane.
499
+ let y = TIME_AXIS_HEIGHT + 12; // default: events strip
500
+ if (ev.taskId) {
501
+ const lane = laneById.get(`task:${ev.taskId}`);
502
+ if (lane) y = TIME_AXIS_HEIGHT + EVENTS_STRIP_HEIGHT + lane.index * LANE_HEIGHT + LANE_HEIGHT / 2;
503
+ } else if (ev.nodeId) {
504
+ const id = String(ev.nodeId);
505
+ const lane = laneById.get(id.startsWith('task:') || id.startsWith('bg:') ? id : `task:${id}`);
506
+ if (lane) y = TIME_AXIS_HEIGHT + EVENTS_STRIP_HEIGHT + lane.index * LANE_HEIGHT + LANE_HEIGHT / 2;
507
+ }
508
+
509
+ out.push({
510
+ id: `${ev.ts}-${ev.kind}-${ev.author ?? ''}-${ev.taskId ?? ''}`,
511
+ x,
512
+ y,
513
+ kind: ev.kind || 'event',
514
+ ts,
515
+ text: String(ev.text || ev.kind || ''),
516
+ author: ev.author as string | undefined,
517
+ });
518
+ }
519
+ return out;
520
+ }, [events, rangeStart, rangeEnd, timeToX, laneById]);
521
+
522
+ // ─── Lane backgrounds (alternating) ─────────────────────────────────
523
+ const laneRects = useMemo(() => {
524
+ return lanes.map((lane) => ({
525
+ id: lane.id,
526
+ y: TIME_AXIS_HEIGHT + EVENTS_STRIP_HEIGHT + lane.index * LANE_HEIGHT,
527
+ h: LANE_HEIGHT,
528
+ }));
529
+ }, [lanes]);
530
+
531
+ // ─── Now line position ──────────────────────────────────────────────
532
+ const nowX = useMemo(() => {
533
+ const x = timeToX(nowTick);
534
+ return Math.max(0, Math.min(canvasSize.width, x));
535
+ }, [timeToX, nowTick, canvasSize.width]);
536
+
537
+ // ─── Click on a bar → open detail panel ─────────────────────────────
538
+ const onSelectBar = useCallback((bar: TaskBar) => {
539
+ const id = bar.id; // 'task:xxx' or 'bg:xxx'
540
+ const item: SelectedItem = {
541
+ id,
542
+ kind: bar.kind,
543
+ label: bar.label,
544
+ status: bar.statusClass,
545
+ data: bar.data,
546
+ };
547
+ setSelectedItem(item);
548
+ }, []);
549
+
550
+ // ─── Detail panel: fetch related data when selection changes ────────
551
+ useEffect(() => {
552
+ if (!selectedItem) {
553
+ setComments([]);
554
+ setBgOutput('');
555
+ setBgArtifactIds([]);
556
+ return;
557
+ }
558
+ setCommentText('');
559
+ setTaskTitle('');
560
+ setBgMessage('');
561
+ setBgArtifactIds([]);
562
+ (async () => {
563
+ try {
564
+ const r = await api.get<{ events: ActivityEvent[] }>(
565
+ `/activity?nodeId=${encodeURIComponent(selectedItem.id)}&limit=50`,
566
+ );
567
+ setComments(r.events || []);
568
+ } catch {
569
+ setComments([]);
570
+ }
571
+ if (selectedItem.kind === 'bg') {
572
+ const bgData = selectedItem.data as BgInstance;
573
+ try {
574
+ const r = await api.get<{ output: string }>(
575
+ `/background/${encodeURIComponent(bgData.instanceId)}/output?lines=80`,
576
+ );
577
+ setBgOutput(r.output || '');
578
+ } catch {
579
+ setBgOutput('');
580
+ }
581
+ // v3.6.2 — If the bg instance has a linked taskId, fetch its artifacts.
582
+ if (bgData.taskId) {
583
+ try {
584
+ const artR = await api.get<{ artifacts: { id: string }[] }>(
585
+ `/tasks/${encodeURIComponent(bgData.taskId)}/artifacts`,
586
+ );
587
+ setBgArtifactIds((artR.artifacts || []).map((a) => a.id));
588
+ } catch {
589
+ setBgArtifactIds([]);
590
+ }
591
+ }
592
+ }
593
+ })();
594
+ }, [selectedItem]);
595
+
596
+ // ─── Detail actions ─────────────────────────────────────────────────
597
+ const onAddComment = async () => {
598
+ if (!selectedItem || !commentText.trim()) return;
599
+ setPostingComment(true);
600
+ try {
601
+ await api.post('/comments', { nodeId: selectedItem.id, text: commentText, author: 'user' });
602
+ setCommentText('');
603
+ const r = await api.get<{ events: ActivityEvent[] }>(
604
+ `/activity?nodeId=${encodeURIComponent(selectedItem.id)}&limit=50`,
605
+ );
606
+ setComments(r.events || []);
607
+ toast.success('Comment added.');
608
+ } catch (err) {
609
+ toast.error(`Comment failed: ${(err as Error).message}`);
610
+ } finally {
611
+ setPostingComment(false);
612
+ }
613
+ };
614
+
615
+ const onCreateTaskFromNode = async () => {
616
+ if (!selectedItem || !taskTitle.trim()) return;
617
+ setCreatingTask(true);
618
+ try {
619
+ await api.post(`/nodes/${encodeURIComponent(selectedItem.id)}/tasks`, {
620
+ title: taskTitle,
621
+ description: `Created from timeline selection ${selectedItem.id}.`,
622
+ priority: taskPriority,
623
+ });
624
+ setTaskTitle('');
625
+ toast.success('Task created.');
626
+ await refreshSnapshot();
627
+ } catch (err) {
628
+ toast.error(`Task create failed: ${(err as Error).message}`);
629
+ } finally {
630
+ setCreatingTask(false);
631
+ }
632
+ };
633
+
634
+ const onSendBgMessage = async () => {
635
+ if (!selectedItem || selectedItem.kind !== 'bg' || !bgMessage.trim()) return;
636
+ const bgData = selectedItem.data as BgInstance;
637
+ try {
638
+ const r = await api.post<{ ok: boolean; error?: string }>(
639
+ `/background/${encodeURIComponent(bgData.instanceId)}/message`,
640
+ { message: bgMessage },
641
+ );
642
+ if (r.ok) {
643
+ toast.success('Message sent.');
644
+ setBgMessage('');
645
+ } else {
646
+ toast.warning(r.error || 'Send failed');
647
+ }
648
+ } catch (err) {
649
+ toast.error(`Send failed: ${(err as Error).message}`);
650
+ }
651
+ };
652
+
653
+ const onKillBg = async () => {
654
+ if (!selectedItem || selectedItem.kind !== 'bg') return;
655
+ if (!confirm('Kill this bg instance session?')) return;
656
+ const bgData = selectedItem.data as BgInstance;
657
+ try {
658
+ const r = await api.del<{ ok: boolean; error?: string }>(
659
+ `/background/${encodeURIComponent(bgData.instanceId)}`,
660
+ );
661
+ if (r.ok) toast.success('Session killed.');
662
+ else toast.warning(r.error || 'Kill failed');
663
+ await reloadAll();
664
+ } catch (err) {
665
+ toast.error(`Kill failed: ${(err as Error).message}`);
666
+ }
667
+ };
668
+
669
+ const refetchOutput = async () => {
670
+ if (!selectedItem || selectedItem.kind !== 'bg') return;
671
+ const bgData = selectedItem.data as BgInstance;
672
+ try {
673
+ const r = await api.get<{ output: string }>(
674
+ `/background/${encodeURIComponent(bgData.instanceId)}/output?lines=80`,
675
+ );
676
+ setBgOutput(r.output || '');
677
+ } catch {
678
+ /* ignore */
679
+ }
680
+ };
681
+
682
+ // ─── Keyboard helpers ───────────────────────────────────────────────
683
+ useEffect(() => {
684
+ const onKey = (e: KeyboardEvent) => {
685
+ if (e.key === 'Escape') setSelectedItem(null);
686
+ };
687
+ window.addEventListener('keydown', onKey);
688
+ return () => window.removeEventListener('keydown', onKey);
689
+ }, []);
690
+
691
+ // ─── Refresh handler ────────────────────────────────────────────────
692
+ const refresh = () => {
693
+ setRefreshTick((t) => t + 1);
694
+ reloadAll();
695
+ };
696
+
697
+ // ─── Derived: total content height (for SVG sizing) ─────────────────
698
+ const contentHeight = TIME_AXIS_HEIGHT + EVENTS_STRIP_HEIGHT + lanes.length * LANE_HEIGHT;
699
+
700
+ // ─── Render ─────────────────────────────────────────────────────────
701
+ return (
702
+ <div className="view view-activity">
703
+ {/* Top header bar — title + live/pause + zoom + refresh */}
704
+ <header className="view-header">
705
+ <div className="view-header-text">
706
+ <h2 className="view-title">
707
+ <ActivityIcon size={18} /> Activity
708
+ </h2>
709
+ <p className="view-subtitle">
710
+ Live timeline of agents, tasks, and background sessions. Active tasks pulse; the red line marks &ldquo;now&rdquo;.
711
+ </p>
712
+ </div>
713
+ <div className="view-actions">
714
+ <div className="tl-mode-toggle">
715
+ <Button
716
+ variant={mode === 'live' ? 'primary' : 'secondary'}
717
+ size="sm"
718
+ onClick={() => setMode(mode === 'live' ? 'pause' : 'live')}
719
+ title={mode === 'live' ? 'Pause timeline' : 'Resume timeline'}
720
+ >
721
+ {mode === 'live' ? <Pause size={14} /> : <Play size={14} />}
722
+ {mode === 'live' ? 'Live' : 'Paused'}
723
+ </Button>
724
+ <div className="tl-zoom-group">
725
+ {(['1m', '5m', '30m', '1h'] as ZoomKey[]).map((z) => (
726
+ <button
727
+ type="button"
728
+ key={z}
729
+ className={cn('tl-zoom-btn', zoom === z && 'tl-zoom-btn-active')}
730
+ onClick={() => setZoom(z)}
731
+ title={`Zoom to ${z}`}
732
+ >
733
+ {z}
734
+ </button>
735
+ ))}
736
+ </div>
737
+ <Button variant="secondary" size="sm" onClick={refresh} title="Refresh">
738
+ <RefreshCw size={14} /> Refresh
739
+ </Button>
740
+ </div>
741
+ </div>
742
+ </header>
743
+
744
+ {loading && (
745
+ <div className="view-loading"><Spinner size="lg" /></div>
746
+ )}
747
+
748
+ {!loading && agents.length === 0 && tasks.length === 0 && bgInstances.length === 0 && events.length === 0 ? (
749
+ <EmptyState
750
+ icon={<ActivityIcon size={32} />}
751
+ title="No activity yet"
752
+ message="Once agents, tasks, or background instances exist, they'll show up here on the timeline."
753
+ />
754
+ ) : (
755
+ <div className={cn('tl-view', selectedItem && 'tl-view-detail-open')}>
756
+ <div className={cn('tl-body', !streamOpen && 'tl-body-stream-collapsed')}>
757
+ {/* Left column — event stream (sibling, not overlay) */}
758
+ <aside className={cn('tl-stream', !streamOpen && 'tl-stream-collapsed')}>
759
+ <div className="tl-stream-head">
760
+ <h3>
761
+ <History size={13} /> Live events
762
+ </h3>
763
+ <button
764
+ type="button"
765
+ className="icon-btn"
766
+ onClick={() => setStreamOpen(false)}
767
+ title="Hide event stream"
768
+ aria-label="Hide event stream"
769
+ >
770
+ <PanelLeftClose size={14} />
771
+ </button>
772
+ </div>
773
+ <div className="tl-stream-list">
774
+ {events.length === 0 ? (
775
+ <div className="tl-stream-empty">
776
+ <p>No events yet.</p>
777
+ </div>
778
+ ) : (
779
+ [...events].reverse().map((ev, i) => {
780
+ const Icon =
781
+ ev.author ? Bot : ev.kind === 'task' ? CheckSquare : ev.kind === 'bg' ? Cpu : ActivityIcon;
782
+ return (
783
+ <div key={`${ev.ts}-${i}`} className="tl-stream-event">
784
+ <span className="tl-stream-event-time">
785
+ {new Date(ev.ts).toLocaleTimeString('en-GB', { hour12: false })}
786
+ </span>
787
+ <span className="tl-stream-event-icon" style={{ color: statusColor(ev.kind) }}>
788
+ <Icon size={12} />
789
+ </span>
790
+ <span className="tl-stream-event-text">
791
+ {ev.author || ev.text || ev.kind}
792
+ </span>
793
+ </div>
794
+ );
795
+ })
796
+ )}
797
+ </div>
798
+ </aside>
799
+
800
+ {/* Center column — timeline canvas */}
801
+ <div className="tl-canvas-wrap" ref={canvasWrapRef}>
802
+ <div className="tl-canvas-toolbar">
803
+ <div className="tl-canvas-toolbar-left">
804
+ {!streamOpen && (
805
+ <Button
806
+ variant="ghost"
807
+ size="sm"
808
+ onClick={() => setStreamOpen(true)}
809
+ title="Show event stream"
810
+ >
811
+ <PanelLeftOpen size={14} /> Events
812
+ </Button>
813
+ )}
814
+ <span className="tl-canvas-mode">
815
+ {mode === 'live' ? '● Live' : '⏸ Paused'}
816
+ </span>
817
+ <span className="tl-canvas-range">
818
+ {new Date(rangeStart).toLocaleTimeString('en-GB', { hour12: false })} →{' '}
819
+ {new Date(rangeEnd).toLocaleTimeString('en-GB', { hour12: false })}
820
+ </span>
821
+ </div>
822
+ <div className="tl-canvas-legend">
823
+ <span className="tl-legend-item"><span className="tl-legend-dot" style={{ background: 'var(--success)' }} /> active</span>
824
+ <span className="tl-legend-item"><span className="tl-legend-dot tl-legend-dot-dashed" style={{ borderColor: 'var(--info)' }} /> queued</span>
825
+ <span className="tl-legend-item"><span className="tl-legend-dot" style={{ background: 'var(--warning)' }} /> blocked</span>
826
+ <span className="tl-legend-item"><span className="tl-legend-dot" style={{ background: 'var(--error)' }} /> error</span>
827
+ <span className="tl-legend-sep" />
828
+ <span className="tl-legend-item"><Layers size={11} /> agent</span>
829
+ <span className="tl-legend-item"><Target size={11} /> task</span>
830
+ <span className="tl-legend-item"><Cpu size={11} /> bg</span>
831
+ </div>
832
+ </div>
833
+
834
+ <div className="tl-canvas-scroll">
835
+ <svg
836
+ className="tl-canvas"
837
+ width={canvasSize.width}
838
+ height={Math.max(canvasSize.height, contentHeight)}
839
+ role="application"
840
+ aria-label="Activity timeline"
841
+ >
842
+ <defs>
843
+ <pattern
844
+ id="tl-canvas-grid"
845
+ width={canvasSize.width}
846
+ height={LANE_HEIGHT}
847
+ patternUnits="userSpaceOnUse"
848
+ >
849
+ <path
850
+ d={`M 0 0 L 0 ${LANE_HEIGHT}`}
851
+ fill="none"
852
+ stroke="var(--border)"
853
+ strokeWidth={0.5}
854
+ opacity={0.35}
855
+ />
856
+ </pattern>
857
+ </defs>
858
+ <rect width="100%" height="100%" fill="url(#tl-canvas-grid)" />
859
+
860
+ {/* Lane backgrounds (alternating) */}
861
+ {laneRects.map((lr) => (
862
+ <rect
863
+ key={`bg-${lr.id}`}
864
+ className="tl-lane-bg"
865
+ x={0}
866
+ y={lr.y}
867
+ width={canvasSize.width}
868
+ height={lr.h}
869
+ fill={lr.id.charCodeAt(lr.id.length - 1) % 2 === 0 ? 'var(--bg-elev-2)' : 'var(--bg-elev)'}
870
+ opacity={0.4}
871
+ />
872
+ ))}
873
+
874
+ {/* Time axis tick lines + labels */}
875
+ <g className="tl-time-axis">
876
+ {ticks.map((t) => (
877
+ <g key={`tick-${t.t}`}>
878
+ <line
879
+ x1={t.x}
880
+ y1={0}
881
+ x2={t.x}
882
+ y2={contentHeight}
883
+ stroke="var(--border)"
884
+ strokeWidth={0.5}
885
+ strokeDasharray="2,4"
886
+ opacity={0.5}
887
+ />
888
+ <text
889
+ x={t.x + 4}
890
+ y={TIME_AXIS_HEIGHT - 8}
891
+ fontSize={10}
892
+ fontFamily="var(--font-mono)"
893
+ fill="var(--text-dim)"
894
+ >
895
+ {t.label}
896
+ </text>
897
+ </g>
898
+ ))}
899
+ </g>
900
+
901
+ {/* Lane labels (rendered inside SVG, x=4) */}
902
+ {lanes.map((lane) => {
903
+ const y = TIME_AXIS_HEIGHT + EVENTS_STRIP_HEIGHT + lane.index * LANE_HEIGHT + LANE_HEIGHT / 2;
904
+ return (
905
+ <g key={`label-${lane.id}`} className="tl-lane-label-group">
906
+ <text
907
+ x={6}
908
+ y={y - 2}
909
+ className="tl-lane-label"
910
+ textAnchor="start"
911
+ >
912
+ {shortLabel(lane.label, 28)}
913
+ </text>
914
+ <text
915
+ x={6}
916
+ y={y + 10}
917
+ className="tl-lane-label-sub"
918
+ textAnchor="start"
919
+ >
920
+ {lane.sub}
921
+ </text>
922
+ </g>
923
+ );
924
+ })}
925
+
926
+ {/* Task / BG bars */}
927
+ {bars.map((bar) => {
928
+ const fillColor = statusColor(bar.statusClass);
929
+ const isSelected = bar.id === selectedItem?.id;
930
+ return (
931
+ <g
932
+ key={`bar-${bar.id}`}
933
+ className={cn('tl-task-bar', `tl-task-bar-${bar.statusClass}`, isSelected && 'tl-task-bar-selected')}
934
+ onClick={() => onSelectBar(bar)}
935
+ >
936
+ <rect
937
+ x={bar.x}
938
+ y={bar.y}
939
+ width={bar.w}
940
+ height={bar.h}
941
+ rx={6}
942
+ fill={fillColor}
943
+ fillOpacity={bar.statusClass === 'done' ? 0.35 : bar.statusClass === 'queued' ? 0.18 : 0.85}
944
+ stroke={fillColor}
945
+ strokeWidth={isSelected ? 2.5 : 1.5}
946
+ strokeOpacity={bar.statusClass === 'done' ? 0.5 : 1}
947
+ strokeDasharray={bar.statusClass === 'queued' ? '4 4' : undefined}
948
+ style={{ cursor: 'pointer' }}
949
+ />
950
+ {bar.w > 36 && (
951
+ <text
952
+ x={bar.x + 8}
953
+ y={bar.y + bar.h / 2 + 4}
954
+ fontSize={11}
955
+ fontFamily="var(--font-sans)"
956
+ fontWeight={500}
957
+ fill="var(--text-strong)"
958
+ style={{ pointerEvents: 'none' }}
959
+ opacity={bar.statusClass === 'done' ? 0.7 : 1}
960
+ >
961
+ {shortLabel(bar.label, Math.max(4, Math.floor(bar.w / 7)))}
962
+ </text>
963
+ )}
964
+ </g>
965
+ );
966
+ })}
967
+
968
+ {/* Event markers */}
969
+ {eventMarkers.map((m) => {
970
+ const color = statusColor(m.kind);
971
+ return (
972
+ <g key={`ev-${m.id}`} className="tl-event-marker-group">
973
+ <line
974
+ x1={m.x}
975
+ y1={m.y - 6}
976
+ x2={m.x}
977
+ y2={m.y + 6}
978
+ stroke={color}
979
+ strokeWidth={1.2}
980
+ opacity={0.7}
981
+ />
982
+ <circle
983
+ cx={m.x}
984
+ cy={m.y}
985
+ r={4}
986
+ fill={color}
987
+ opacity={0.9}
988
+ >
989
+ <title>
990
+ {`${m.kind}${m.author ? ` · ${m.author}` : ''} · ${new Date(m.ts).toLocaleTimeString('en-GB', { hour12: false })}`}
991
+ </title>
992
+ </circle>
993
+ </g>
994
+ );
995
+ })}
996
+
997
+ {/* Now line (red, vertical) */}
998
+ <line
999
+ className="tl-now-line"
1000
+ x1={nowX}
1001
+ y1={0}
1002
+ x2={nowX}
1003
+ y2={contentHeight}
1004
+ stroke="var(--error)"
1005
+ strokeWidth={1.5}
1006
+ opacity={0.9}
1007
+ />
1008
+ <g className="tl-now-marker">
1009
+ <circle cx={nowX} cy={TIME_AXIS_HEIGHT - 4} r={4} fill="var(--error)" />
1010
+ <text
1011
+ x={nowX + 6}
1012
+ y={TIME_AXIS_HEIGHT - 4}
1013
+ fontSize={10}
1014
+ fontWeight={700}
1015
+ fill="var(--error)"
1016
+ fontFamily="var(--font-mono)"
1017
+ >
1018
+ now
1019
+ </text>
1020
+ </g>
1021
+ </svg>
1022
+ </div>
1023
+ </div>
1024
+
1025
+ {/* Right column — detail panel */}
1026
+ {selectedItem && (
1027
+ <aside className="tl-detail tl-detail-enter" key={selectedItem.id}>
1028
+ <Card>
1029
+ <CardTitle>
1030
+ {selectedItem.kind === 'agent' && <Bot size={14} />}
1031
+ {selectedItem.kind === 'task' && <CheckSquare size={14} />}
1032
+ {selectedItem.kind === 'bg' && <Cpu size={14} />}
1033
+ {selectedItem.label}
1034
+ <button
1035
+ type="button"
1036
+ className="icon-btn"
1037
+ onClick={() => setSelectedItem(null)}
1038
+ title="Close"
1039
+ aria-label="Close activity detail"
1040
+ style={{ marginLeft: 'auto' }}
1041
+ >
1042
+ <X size={14} />
1043
+ </button>
1044
+ </CardTitle>
1045
+ <div className="tl-detail-meta">
1046
+ <div><span className="muted">type</span> {selectedItem.kind}</div>
1047
+ <div><span className="muted">status</span> <code>{selectedItem.status}</code></div>
1048
+ {selectedItem.kind === 'agent' && (() => {
1049
+ const a = selectedItem.data as Agent;
1050
+ return (
1051
+ <>
1052
+ {a.role && <div><span className="muted">role</span> {a.role}</div>}
1053
+ {a.model && <div><span className="muted">model</span> {a.model}</div>}
1054
+ </>
1055
+ );
1056
+ })()}
1057
+ {selectedItem.kind === 'task' && (() => {
1058
+ const t = selectedItem.data as Task;
1059
+ return (
1060
+ <>
1061
+ {t.assignee && <div><span className="muted">assignee</span> @{t.assignee}</div>}
1062
+ {t.priority && <div><span className="muted">priority</span> {t.priority}</div>}
1063
+ {t.createdAt && <div><span className="muted">created</span> {new Date(t.createdAt).toLocaleString()}</div>}
1064
+ </>
1065
+ );
1066
+ })()}
1067
+ {selectedItem.kind === 'bg' && (() => {
1068
+ const b = selectedItem.data as BgInstance;
1069
+ return (
1070
+ <>
1071
+ {b.startedAt && <div><span className="muted">started</span> {new Date(b.startedAt).toLocaleString()}</div>}
1072
+ {b.tmuxSession && <div><span className="muted">tmux</span> <code>{b.tmuxSession}</code> {b.tmuxActive ? <span className="tag tag-success">active</span> : <span className="tag">inactive</span>}</div>}
1073
+ </>
1074
+ );
1075
+ })()}
1076
+ </div>
1077
+ {selectedItem.kind === 'task' && (selectedItem.data as Task).description && (
1078
+ <div className="tl-detail-desc">{(selectedItem.data as Task).description}</div>
1079
+ )}
1080
+ {selectedItem.kind === 'bg' && (selectedItem.data as BgInstance).promptPreview && (
1081
+ <div className="tl-detail-desc">{(selectedItem.data as BgInstance).promptPreview}</div>
1082
+ )}
1083
+
1084
+ {selectedItem.kind === 'bg' && (
1085
+ <div className="tl-detail-bg">
1086
+ <pre className="tl-bg-output">{bgOutput || '(no output — start the session via tmux attach)'}</pre>
1087
+ <div className="tl-bg-output-actions">
1088
+ <Button variant="ghost" size="sm" onClick={refetchOutput}>
1089
+ <RefreshCw size={12} /> Refresh output
1090
+ </Button>
1091
+ {bgArtifactIds.length > 0 && (
1092
+ <Button
1093
+ variant="secondary"
1094
+ size="sm"
1095
+ onClick={() => openArtifactViewer(modal, bgArtifactIds[0])}
1096
+ >
1097
+ <FileText size={12} /> Open artifact
1098
+ </Button>
1099
+ )}
1100
+ <Button variant="danger" size="sm" onClick={onKillBg}>
1101
+ <Trash2 size={12} /> Kill session
1102
+ </Button>
1103
+ </div>
1104
+ <div className="tl-form-row">
1105
+ <input
1106
+ className="input"
1107
+ placeholder="Send a message to this bg session…"
1108
+ value={bgMessage}
1109
+ onChange={(e) => setBgMessage(e.target.value)}
1110
+ onKeyDown={(e) => {
1111
+ if (e.key === 'Enter') onSendBgMessage();
1112
+ }}
1113
+ />
1114
+ <Button variant="primary" size="sm" disabled={!bgMessage.trim()} onClick={onSendBgMessage}>
1115
+ <Send size={12} /> Send
1116
+ </Button>
1117
+ </div>
1118
+ </div>
1119
+ )}
1120
+
1121
+ {selectedItem.kind !== 'bg' && (
1122
+ <div className="tl-detail-create">
1123
+ <div className="field-label">Create follow-up task</div>
1124
+ <input
1125
+ className="input"
1126
+ placeholder="Task title"
1127
+ value={taskTitle}
1128
+ onChange={(e) => setTaskTitle(e.target.value)}
1129
+ />
1130
+ <div className="tl-form-row">
1131
+ <select
1132
+ className="select"
1133
+ value={taskPriority}
1134
+ onChange={(e) => setTaskPriority(e.target.value)}
1135
+ >
1136
+ <option value="low">Low</option>
1137
+ <option value="normal">Normal</option>
1138
+ <option value="high">High</option>
1139
+ </select>
1140
+ <Button
1141
+ variant="primary"
1142
+ size="sm"
1143
+ disabled={!taskTitle.trim() || creatingTask}
1144
+ onClick={onCreateTaskFromNode}
1145
+ >
1146
+ <Plus size={12} /> Add task
1147
+ </Button>
1148
+ </div>
1149
+ </div>
1150
+ )}
1151
+
1152
+ <div className="tl-detail-comments">
1153
+ <div className="field-label">
1154
+ <MessageSquare size={12} /> Comments &amp; activity
1155
+ </div>
1156
+ <ul className="comment-list">
1157
+ {comments.length === 0 && <li className="muted">No comments yet.</li>}
1158
+ {comments.map((c, i) => (
1159
+ <li key={`c-${i}`} className="comment-item">
1160
+ <div className="comment-head">
1161
+ <strong>{c.author || 'system'}</strong>
1162
+ <span className="muted">
1163
+ {c.kind} · {new Date(c.ts).toLocaleString()}
1164
+ </span>
1165
+ </div>
1166
+ {c.text && <div className="comment-text">{c.text}</div>}
1167
+ {c.taskId && (
1168
+ <div className="muted">
1169
+ → task <code>{String(c.taskId)}</code>
1170
+ </div>
1171
+ )}
1172
+ </li>
1173
+ ))}
1174
+ </ul>
1175
+ <div className="comment-input-row">
1176
+ <input
1177
+ className="input"
1178
+ placeholder="Add a comment…"
1179
+ value={commentText}
1180
+ onChange={(e) => setCommentText(e.target.value)}
1181
+ onKeyDown={(e) => {
1182
+ if (e.key === 'Enter' && !e.shiftKey) onAddComment();
1183
+ }}
1184
+ />
1185
+ <Button
1186
+ variant="secondary"
1187
+ size="sm"
1188
+ disabled={!commentText.trim() || postingComment}
1189
+ onClick={onAddComment}
1190
+ >
1191
+ <Send size={12} /> Post
1192
+ </Button>
1193
+ </div>
1194
+ </div>
1195
+ </Card>
1196
+ </aside>
1197
+ )}
1198
+ </div>
1199
+ </div>
1200
+ )}
1201
+ </div>
1202
+ );
1203
+ }