vibepulse 0.2.0 → 0.2.1

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 (420) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +4 -0
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/cache/.previewinfo +1 -1
  5. package/.next/cache/.rscinfo +1 -1
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/fallback-build-manifest.json +2 -2
  9. package/.next/prerender-manifest.json +3 -3
  10. package/.next/routes-manifest.json +32 -0
  11. package/.next/server/app/_global-error/page.js +1 -1
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error.html +2 -2
  14. package/.next/server/app/_global-error.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.rsc +2 -2
  25. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  28. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/api/node/events/route.js +1 -1
  32. package/.next/server/app/api/node/events/route.js.nft.json +1 -1
  33. package/.next/server/app/api/node/health/route.js +1 -1
  34. package/.next/server/app/api/node/health/route.js.nft.json +1 -1
  35. package/.next/server/app/api/node/sessions/[id]/archive/route/app-paths-manifest.json +3 -0
  36. package/.next/server/app/api/node/sessions/[id]/archive/route/build-manifest.json +11 -0
  37. package/.next/server/app/api/node/sessions/[id]/archive/route/server-reference-manifest.json +4 -0
  38. package/.next/server/app/api/node/sessions/[id]/archive/route.js +6 -0
  39. package/.next/server/app/api/node/sessions/[id]/archive/route.js.map +5 -0
  40. package/.next/server/app/api/node/sessions/[id]/archive/route.js.nft.json +1 -0
  41. package/.next/server/app/api/node/sessions/[id]/archive/route_client-reference-manifest.js +2 -0
  42. package/.next/server/app/api/node/sessions/[id]/delete/route/app-paths-manifest.json +3 -0
  43. package/.next/server/app/api/node/sessions/[id]/delete/route/build-manifest.json +11 -0
  44. package/.next/server/app/api/node/sessions/[id]/delete/route/server-reference-manifest.json +4 -0
  45. package/.next/server/app/api/node/sessions/[id]/delete/route.js +7 -0
  46. package/.next/server/app/api/node/sessions/[id]/delete/route.js.map +5 -0
  47. package/.next/server/app/api/node/sessions/[id]/delete/route.js.nft.json +1 -0
  48. package/.next/server/app/api/node/sessions/[id]/delete/route_client-reference-manifest.js +2 -0
  49. package/.next/server/app/api/node/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  50. package/.next/server/app/api/node/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  51. package/.next/server/app/api/node/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  52. package/.next/server/app/api/node/sessions/[id]/open-editor/route.js +7 -0
  53. package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.map +5 -0
  54. package/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -0
  55. package/.next/server/app/api/node/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  56. package/.next/server/app/api/node/sessions/route.js +1 -1
  57. package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/opencode-events/route.js +1 -1
  59. package/.next/server/app/api/opencode-events/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/[id]/archive/route.js +2 -1
  61. package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  63. package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  65. package/.next/server/app/api/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  66. package/.next/server/app/api/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  67. package/.next/server/app/api/sessions/[id]/open-editor/route.js +7 -0
  68. package/.next/server/app/api/sessions/[id]/open-editor/route.js.map +5 -0
  69. package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -0
  70. package/.next/server/app/api/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  71. package/.next/server/app/api/sessions/route.js +1 -1
  72. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  73. package/.next/server/app/index.html +1 -1
  74. package/.next/server/app/index.rsc +3 -3
  75. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  76. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  77. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  79. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  80. package/.next/server/app/page.js +1 -1
  81. package/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/server/app-paths-manifest.json +4 -0
  84. package/.next/server/chunks/[root-of-the-server]__1211da38._.js +3 -0
  85. package/.next/server/chunks/[root-of-the-server]__1211da38._.js.map +1 -0
  86. package/.next/{standalone/.next/server/chunks/[root-of-the-server]__b698889b._.js → server/chunks/[root-of-the-server]__1b87ec42._.js} +2 -2
  87. package/.next/server/chunks/[root-of-the-server]__1b87ec42._.js.map +1 -0
  88. package/.next/server/chunks/[root-of-the-server]__2b526e7a._.js +3 -0
  89. package/.next/server/chunks/[root-of-the-server]__2b526e7a._.js.map +1 -0
  90. package/.next/server/chunks/[root-of-the-server]__2f981540._.js +3 -0
  91. package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +1 -0
  92. package/.next/server/chunks/[root-of-the-server]__3745b314._.js +3 -0
  93. package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +1 -0
  94. package/.next/server/chunks/[root-of-the-server]__56690af0._.js +1 -1
  95. package/.next/server/chunks/[root-of-the-server]__56690af0._.js.map +1 -1
  96. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  97. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
  98. package/.next/server/chunks/[root-of-the-server]__59175de4._.js +1 -1
  99. package/.next/server/chunks/[root-of-the-server]__59175de4._.js.map +1 -1
  100. package/.next/server/chunks/[root-of-the-server]__64fffc02._.js +1 -1
  101. package/.next/server/chunks/[root-of-the-server]__64fffc02._.js.map +1 -1
  102. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +3 -0
  103. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +1 -0
  104. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +3 -0
  105. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +1 -0
  106. package/.next/server/chunks/[root-of-the-server]__89c5eeab._.js +1 -1
  107. package/.next/server/chunks/[root-of-the-server]__89c5eeab._.js.map +1 -1
  108. package/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js +1 -1
  109. package/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js.map +1 -1
  110. package/.next/server/chunks/[root-of-the-server]__b796d06c._.js +1 -1
  111. package/.next/server/chunks/[root-of-the-server]__b796d06c._.js.map +1 -1
  112. package/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js +1 -1
  113. package/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js.map +1 -1
  114. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +3 -0
  115. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -0
  116. package/.next/server/chunks/[root-of-the-server]__db285678._.js +3 -0
  117. package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +1 -0
  118. package/.next/{standalone/.next/server/chunks/[root-of-the-server]__16a9eb0a._.js → server/chunks/[root-of-the-server]__e00a9200._.js} +2 -2
  119. package/.next/server/chunks/{[root-of-the-server]__16a9eb0a._.js.map → [root-of-the-server]__e00a9200._.js.map} +1 -1
  120. package/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js +3 -0
  121. package/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js.map +1 -0
  122. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_archive_route_actions_237255a5.js +3 -0
  123. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_archive_route_actions_237255a5.js.map +1 -0
  124. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_delete_route_actions_e5d426f6.js +3 -0
  125. package/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_delete_route_actions_e5d426f6.js.map +1 -0
  126. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_open-editor_route_actions_eaebf476.js +3 -0
  127. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_open-editor_route_actions_eaebf476.js.map +1 -0
  128. package/.next/server/chunks/ce889_server_app_api_node_sessions_[id]_open-editor_route_actions_791cdf5b.js +3 -0
  129. package/.next/server/chunks/ce889_server_app_api_node_sessions_[id]_open-editor_route_actions_791cdf5b.js.map +1 -0
  130. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js +1 -1
  131. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js.map +1 -1
  132. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  133. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
  134. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js +2 -2
  135. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js.map +1 -1
  136. package/.next/server/chunks/ssr/{[root-of-the-server]__efc52f08._.js → [root-of-the-server]__631e12d0._.js} +2 -2
  137. package/.next/server/chunks/ssr/[root-of-the-server]__a8cd3911._.js +3 -0
  138. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  139. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
  140. package/.next/server/pages/404.html +1 -1
  141. package/.next/server/pages/500.html +2 -2
  142. package/.next/server/server-reference-manifest.js +1 -1
  143. package/.next/server/server-reference-manifest.json +1 -1
  144. package/.next/standalone/.next/BUILD_ID +1 -1
  145. package/.next/standalone/.next/app-path-routes-manifest.json +4 -0
  146. package/.next/standalone/.next/build-manifest.json +2 -2
  147. package/.next/standalone/.next/prerender-manifest.json +3 -3
  148. package/.next/standalone/.next/routes-manifest.json +32 -0
  149. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  150. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  151. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  152. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  153. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  154. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  155. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  156. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  157. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  158. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  159. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  160. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  161. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  162. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  163. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  164. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  165. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  166. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  167. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  168. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  169. package/.next/standalone/.next/server/app/api/node/events/route.js +1 -1
  170. package/.next/standalone/.next/server/app/api/node/events/route.js.nft.json +1 -1
  171. package/.next/standalone/.next/server/app/api/node/health/route.js +1 -1
  172. package/.next/standalone/.next/server/app/api/node/health/route.js.nft.json +1 -1
  173. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route/app-paths-manifest.json +3 -0
  174. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route/build-manifest.json +11 -0
  175. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route/server-reference-manifest.json +4 -0
  176. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route.js +6 -0
  177. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route.js.map +5 -0
  178. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route.js.nft.json +1 -0
  179. package/.next/standalone/.next/server/app/api/node/sessions/[id]/archive/route_client-reference-manifest.js +2 -0
  180. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route/app-paths-manifest.json +3 -0
  181. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route/build-manifest.json +11 -0
  182. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route/server-reference-manifest.json +4 -0
  183. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js +7 -0
  184. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js.map +5 -0
  185. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route.js.nft.json +1 -0
  186. package/.next/standalone/.next/server/app/api/node/sessions/[id]/delete/route_client-reference-manifest.js +2 -0
  187. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  188. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  189. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  190. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js +7 -0
  191. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.map +5 -0
  192. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -0
  193. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  194. package/.next/standalone/.next/server/app/api/node/sessions/route.js +1 -1
  195. package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  196. package/.next/standalone/.next/server/app/api/opencode-events/route.js +1 -1
  197. package/.next/standalone/.next/server/app/api/opencode-events/route.js.nft.json +1 -1
  198. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +2 -1
  199. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  200. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  201. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  202. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route/app-paths-manifest.json +3 -0
  203. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route/build-manifest.json +11 -0
  204. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route/server-reference-manifest.json +4 -0
  205. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +7 -0
  206. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.map +5 -0
  207. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -0
  208. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route_client-reference-manifest.js +2 -0
  209. package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
  210. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  211. package/.next/standalone/.next/server/app/index.html +1 -1
  212. package/.next/standalone/.next/server/app/index.rsc +3 -3
  213. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  214. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  215. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  216. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  217. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  218. package/.next/standalone/.next/server/app/page.js +1 -1
  219. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  220. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  221. package/.next/standalone/.next/server/app-paths-manifest.json +4 -0
  222. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1211da38._.js +3 -0
  223. package/.next/{server/chunks/[root-of-the-server]__b698889b._.js → standalone/.next/server/chunks/[root-of-the-server]__1b87ec42._.js} +2 -2
  224. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2b526e7a._.js +3 -0
  225. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +3 -0
  226. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +3 -0
  227. package/.next/standalone/.next/server/chunks/[root-of-the-server]__56690af0._.js +1 -1
  228. package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  229. package/.next/standalone/.next/server/chunks/[root-of-the-server]__59175de4._.js +1 -1
  230. package/.next/standalone/.next/server/chunks/[root-of-the-server]__64fffc02._.js +1 -1
  231. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +3 -0
  232. package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +3 -0
  233. package/.next/standalone/.next/server/chunks/[root-of-the-server]__89c5eeab._.js +1 -1
  234. package/.next/standalone/.next/server/chunks/[root-of-the-server]__8da6c5a8._.js +1 -1
  235. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b796d06c._.js +1 -1
  236. package/.next/standalone/.next/server/chunks/[root-of-the-server]__c2ce5c0f._.js +1 -1
  237. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +3 -0
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +3 -0
  239. package/.next/{server/chunks/[root-of-the-server]__16a9eb0a._.js → standalone/.next/server/chunks/[root-of-the-server]__e00a9200._.js} +2 -2
  240. package/.next/standalone/.next/server/chunks/[root-of-the-server]__e5df5e5f._.js +3 -0
  241. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_archive_route_actions_237255a5.js +3 -0
  242. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_node_sessions_[id]_delete_route_actions_e5d426f6.js +3 -0
  243. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_open-editor_route_actions_eaebf476.js +3 -0
  244. package/.next/standalone/.next/server/chunks/ce889_server_app_api_node_sessions_[id]_open-editor_route_actions_791cdf5b.js +3 -0
  245. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7e181e75.js +1 -1
  246. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  247. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_fa835ac3.js +2 -2
  248. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__efc52f08._.js → [root-of-the-server]__631e12d0._.js} +2 -2
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__a8cd3911._.js +3 -0
  250. package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  251. package/.next/standalone/.next/server/pages/404.html +1 -1
  252. package/.next/standalone/.next/server/pages/500.html +2 -2
  253. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  254. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  255. package/.next/standalone/.next/static/chunks/7ac19aaef01f4a03.js +13 -0
  256. package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +3 -0
  257. package/.next/standalone/AGENTS.md +85 -0
  258. package/.next/standalone/README.md +76 -0
  259. package/.next/standalone/__mocks__/child_process.ts +4 -0
  260. package/.next/standalone/bin/dev-runtime.js +58 -0
  261. package/.next/standalone/bin/vibepulse.js +87 -0
  262. package/.next/standalone/check-hsql.mjs +71 -0
  263. package/.next/standalone/docs/session-status-detection.md +258 -0
  264. package/.next/standalone/eslint.config.mjs +31 -0
  265. package/.next/standalone/next.config.ts +8 -0
  266. package/.next/standalone/package-lock.json +10312 -0
  267. package/.next/standalone/package.json +1 -1
  268. package/.next/standalone/postcss.config.mjs +7 -0
  269. package/.next/standalone/src/AGENTS.md +41 -0
  270. package/.next/standalone/src/app/api/AGENTS.md +40 -0
  271. package/.next/standalone/src/app/api/node/events/route.test.ts +196 -0
  272. package/.next/standalone/src/app/api/node/events/route.ts +259 -0
  273. package/.next/standalone/src/app/api/node/health/route.test.ts +190 -0
  274. package/.next/standalone/src/app/api/node/health/route.ts +48 -0
  275. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +128 -0
  276. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +97 -0
  277. package/.next/standalone/src/app/api/node/sessions/[id]/delete/route.test.ts +113 -0
  278. package/.next/standalone/src/app/api/node/sessions/[id]/delete/route.ts +81 -0
  279. package/.next/standalone/src/app/api/node/sessions/[id]/open-editor/route.test.ts +206 -0
  280. package/.next/standalone/src/app/api/node/sessions/[id]/open-editor/route.ts +123 -0
  281. package/.next/standalone/src/app/api/node/sessions/route.test.ts +408 -0
  282. package/.next/standalone/src/app/api/node/sessions/route.ts +1094 -0
  283. package/.next/standalone/src/app/api/nodes/route.test.ts +237 -0
  284. package/.next/standalone/src/app/api/nodes/route.ts +176 -0
  285. package/.next/standalone/src/app/api/opencode-config/route.test.ts +86 -0
  286. package/.next/standalone/src/app/api/opencode-config/route.ts +376 -0
  287. package/.next/standalone/src/app/api/opencode-config/status/route.ts +31 -0
  288. package/.next/standalone/src/app/api/opencode-events/route.test.ts +624 -0
  289. package/.next/standalone/src/app/api/opencode-events/route.ts +508 -0
  290. package/.next/standalone/src/app/api/opencode-models/route.test.ts +167 -0
  291. package/.next/standalone/src/app/api/opencode-models/route.ts +76 -0
  292. package/.next/standalone/src/app/api/profiles/[id]/apply/route.ts +49 -0
  293. package/.next/standalone/src/app/api/profiles/[id]/export/route.ts +31 -0
  294. package/.next/standalone/src/app/api/profiles/[id]/route.ts +160 -0
  295. package/.next/standalone/src/app/api/profiles/import/route.test.js +107 -0
  296. package/.next/standalone/src/app/api/profiles/import/route.ts +65 -0
  297. package/.next/standalone/src/app/api/profiles/route.ts +107 -0
  298. package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +136 -0
  299. package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +170 -0
  300. package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +113 -0
  301. package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +137 -0
  302. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +218 -0
  303. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +85 -0
  304. package/.next/standalone/src/app/api/sessions/[id]/route.test.ts +531 -0
  305. package/.next/standalone/src/app/api/sessions/[id]/route.ts +75 -0
  306. package/.next/standalone/src/app/api/sessions/route.test.ts +1298 -0
  307. package/.next/standalone/src/app/api/sessions/route.ts +1695 -0
  308. package/.next/standalone/src/app/favicon.ico +0 -0
  309. package/.next/standalone/src/app/globals.css +66 -0
  310. package/.next/standalone/src/app/layout.tsx +37 -0
  311. package/.next/standalone/src/app/page.test.tsx +134 -0
  312. package/.next/standalone/src/app/page.tsx +358 -0
  313. package/.next/standalone/src/components/AGENTS.md +42 -0
  314. package/.next/standalone/src/components/ErrorBoundary.tsx +72 -0
  315. package/.next/standalone/src/components/KanbanBoard.test.tsx +704 -0
  316. package/.next/standalone/src/components/KanbanBoard.tsx +852 -0
  317. package/.next/standalone/src/components/LoadingState.tsx +37 -0
  318. package/.next/standalone/src/components/ProjectCard.test.tsx +773 -0
  319. package/.next/standalone/src/components/ProjectCard.tsx +595 -0
  320. package/.next/standalone/src/components/QueryProvider.tsx +25 -0
  321. package/.next/standalone/src/components/SessionCard.test.tsx +566 -0
  322. package/.next/standalone/src/components/SessionCard.tsx +434 -0
  323. package/.next/standalone/src/components/SessionList.tsx +60 -0
  324. package/.next/standalone/src/components/host-config/HostManagerDialog.test.tsx +252 -0
  325. package/.next/standalone/src/components/host-config/HostManagerDialog.tsx +476 -0
  326. package/.next/standalone/src/components/opencode-config/AgentConfigForm.test.tsx +72 -0
  327. package/.next/standalone/src/components/opencode-config/AgentConfigForm.tsx +483 -0
  328. package/.next/standalone/src/components/opencode-config/AgentModelSelector.tsx +284 -0
  329. package/.next/standalone/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
  330. package/.next/standalone/src/components/opencode-config/ConfigButton.tsx +43 -0
  331. package/.next/standalone/src/components/opencode-config/ConfigPanel.tsx +91 -0
  332. package/.next/standalone/src/components/opencode-config/FullscreenConfigPanel.tsx +435 -0
  333. package/.next/standalone/src/components/opencode-config/GeneralSettingsForm.test.tsx +91 -0
  334. package/.next/standalone/src/components/opencode-config/GeneralSettingsForm.tsx +288 -0
  335. package/.next/standalone/src/components/opencode-config/categories/CategoriesList.tsx +382 -0
  336. package/.next/standalone/src/components/opencode-config/categories/CategoriesManager.test.tsx +111 -0
  337. package/.next/standalone/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
  338. package/.next/standalone/src/components/opencode-config/categories/CategoryConfigForm.tsx +453 -0
  339. package/.next/standalone/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
  340. package/.next/standalone/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
  341. package/.next/standalone/src/components/opencode-config/profiles/ProfileList.tsx +446 -0
  342. package/.next/standalone/src/components/opencode-config/profiles/ProfileManager.test.tsx +225 -0
  343. package/.next/standalone/src/components/opencode-config/profiles/ProfileManager.tsx +405 -0
  344. package/.next/standalone/src/components/ui/Tabs.tsx +59 -0
  345. package/.next/standalone/src/hooks/useHostSources.test.ts +509 -0
  346. package/.next/standalone/src/hooks/useHostSources.ts +299 -0
  347. package/.next/standalone/src/hooks/useOpencodeSync.test.ts +387 -0
  348. package/.next/standalone/src/hooks/useOpencodeSync.ts +571 -0
  349. package/.next/standalone/src/index.ts +2 -0
  350. package/.next/standalone/src/lib/editorLauncher.server.ts +36 -0
  351. package/.next/standalone/src/lib/editorLauncher.test.ts +35 -0
  352. package/.next/standalone/src/lib/editorLauncher.ts +25 -0
  353. package/.next/standalone/src/lib/hostAccent.test.ts +58 -0
  354. package/.next/standalone/src/lib/hostAccent.ts +46 -0
  355. package/.next/standalone/src/lib/hostIdentity.test.ts +187 -0
  356. package/.next/standalone/src/lib/hostIdentity.ts +122 -0
  357. package/.next/standalone/src/lib/hostSourcesStorage.test.ts +141 -0
  358. package/.next/standalone/src/lib/hostSourcesStorage.ts +72 -0
  359. package/.next/standalone/src/lib/nodeProtocol.test.ts +159 -0
  360. package/.next/standalone/src/lib/nodeProtocol.ts +142 -0
  361. package/.next/standalone/src/lib/nodeRegistry.test.ts +173 -0
  362. package/.next/standalone/src/lib/nodeRegistry.ts +398 -0
  363. package/.next/standalone/src/lib/notificationSound.ts +292 -0
  364. package/.next/standalone/src/lib/opencodeConfig.test.ts +100 -0
  365. package/.next/standalone/src/lib/opencodeConfig.ts +76 -0
  366. package/.next/standalone/src/lib/opencodeDiscovery.ts +275 -0
  367. package/.next/standalone/src/lib/profiles/share.test.ts +91 -0
  368. package/.next/standalone/src/lib/profiles/share.ts +93 -0
  369. package/.next/standalone/src/lib/profiles/storage.test.ts +108 -0
  370. package/.next/standalone/src/lib/profiles/storage.ts +370 -0
  371. package/.next/standalone/src/lib/runtimeMode.test.ts +29 -0
  372. package/.next/standalone/src/lib/runtimeMode.ts +29 -0
  373. package/.next/standalone/src/lib/sessionActionErrors.ts +37 -0
  374. package/.next/standalone/src/lib/sessionArchiveOverrides.test.ts +43 -0
  375. package/.next/standalone/src/lib/sessionArchiveOverrides.ts +116 -0
  376. package/.next/standalone/src/lib/transform.test.ts +121 -0
  377. package/.next/standalone/src/lib/transform.ts +193 -0
  378. package/.next/standalone/src/test/setup.ts +8 -0
  379. package/.next/standalone/src/types/index.ts +152 -0
  380. package/.next/standalone/src/types/opencodeConfig.ts +149 -0
  381. package/.next/standalone/tsconfig.json +34 -0
  382. package/.next/standalone/tsconfig.lib.json +17 -0
  383. package/.next/standalone/vitest.config.ts +16 -0
  384. package/.next/static/chunks/7ac19aaef01f4a03.js +13 -0
  385. package/.next/static/chunks/f42202943f6742e5.css +3 -0
  386. package/.next/trace +1 -1
  387. package/.next/trace-build +1 -1
  388. package/.next/types/routes.d.ts +5 -1
  389. package/.next/types/validator.ts +36 -0
  390. package/package.json +1 -1
  391. package/.next/server/chunks/[root-of-the-server]__0b017945._.js +0 -3
  392. package/.next/server/chunks/[root-of-the-server]__0b017945._.js.map +0 -1
  393. package/.next/server/chunks/[root-of-the-server]__1e118bd3._.js +0 -3
  394. package/.next/server/chunks/[root-of-the-server]__1e118bd3._.js.map +0 -1
  395. package/.next/server/chunks/[root-of-the-server]__6979e732._.js +0 -3
  396. package/.next/server/chunks/[root-of-the-server]__6979e732._.js.map +0 -1
  397. package/.next/server/chunks/[root-of-the-server]__a7b4d79d._.js +0 -3
  398. package/.next/server/chunks/[root-of-the-server]__a7b4d79d._.js.map +0 -1
  399. package/.next/server/chunks/[root-of-the-server]__b698889b._.js.map +0 -1
  400. package/.next/server/chunks/[root-of-the-server]__ddc251b7._.js +0 -3
  401. package/.next/server/chunks/[root-of-the-server]__ddc251b7._.js.map +0 -1
  402. package/.next/server/chunks/ssr/[root-of-the-server]__b0788643._.js +0 -3
  403. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b017945._.js +0 -3
  404. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1e118bd3._.js +0 -3
  405. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6979e732._.js +0 -3
  406. package/.next/standalone/.next/server/chunks/[root-of-the-server]__a7b4d79d._.js +0 -3
  407. package/.next/standalone/.next/server/chunks/[root-of-the-server]__ddc251b7._.js +0 -3
  408. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__b0788643._.js +0 -3
  409. package/.next/standalone/.next/static/chunks/9f8c22002e7395e8.js +0 -13
  410. package/.next/standalone/.next/static/chunks/f1b55db60e7ed6f3.css +0 -3
  411. package/.next/static/chunks/9f8c22002e7395e8.js +0 -13
  412. package/.next/static/chunks/f1b55db60e7ed6f3.css +0 -3
  413. /package/.next/server/chunks/ssr/{[root-of-the-server]__efc52f08._.js.map → [root-of-the-server]__631e12d0._.js.map} +0 -0
  414. /package/.next/server/chunks/ssr/{[root-of-the-server]__b0788643._.js.map → [root-of-the-server]__a8cd3911._.js.map} +0 -0
  415. /package/.next/standalone/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_buildManifest.js +0 -0
  416. /package/.next/standalone/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_clientMiddlewareManifest.json +0 -0
  417. /package/.next/standalone/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_ssgManifest.js +0 -0
  418. /package/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_buildManifest.js +0 -0
  419. /package/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_clientMiddlewareManifest.json +0 -0
  420. /package/.next/static/{L_tmqf71LaeMzApO4SiU- → Fw2R3y-fHX4B2SWxNy_4X}/_ssgManifest.js +0 -0
@@ -0,0 +1,852 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { KanbanColumn, KanbanCard, OpencodeSession } from '@/types';
5
+ import { ProjectCard } from './ProjectCard';
6
+ import { transformSessions } from '@/lib/transform';
7
+ import { LoadingState } from './LoadingState';
8
+ import { playCompleteSound } from '@/lib/notificationSound';
9
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
10
+ import { getSseStatusSnapshot } from '@/hooks/useOpencodeSync';
11
+ import { useHostSources } from '@/hooks/useHostSources';
12
+ import { composeSourceKey } from '@/lib/hostIdentity';
13
+ import { getHostAccentTextClass } from '@/lib/hostAccent';
14
+
15
+ const WAITING_STORAGE_KEY = 'vibepulse:waiting-sessions:v2';
16
+ const SNAPSHOT_STORAGE_KEY = 'vibepulse:last-sessions-snapshot:v2';
17
+ const START_COMMAND_TEMPLATE = 'opencode --port <PORT>';
18
+ const CARD_ANIMATION_DURATION_MS = 250;
19
+ const SESSIONS_ERROR_DISPLAY_THRESHOLD = 3;
20
+ const DEGRADED_MERGE_MAX_SNAPSHOT_AGE_MS = 10 * 60 * 1000;
21
+ const WAITING_PERSIST_MAX_AGE_MS = 10 * 60 * 1000;
22
+
23
+ const LOCAL_SOURCE = {
24
+ hostId: 'local',
25
+ hostLabel: 'Local',
26
+ hostKind: 'local',
27
+ } as const;
28
+
29
+ const COLUMNS: { id: KanbanColumn; title: string }[] = [
30
+ { id: 'idle', title: 'Idle' },
31
+ { id: 'busy', title: 'Busy' },
32
+ { id: 'review', title: 'Needs Attention' },
33
+ { id: 'done', title: 'Archived' },
34
+ ];
35
+
36
+ interface KanbanBoardProps {
37
+ filterDays: number;
38
+ onProcessHintsChange?: (hints: ProcessHint[]) => void;
39
+ hostSources: ReturnType<typeof useHostSources>;
40
+ isNodeMode?: boolean;
41
+ showHostFilter?: boolean;
42
+ onHostStatusesChange?: (statuses: SessionHostStatus[]) => void;
43
+ }
44
+
45
+ type SessionsFetchError = Error & {
46
+ kind?: 'opencode_unavailable' | 'request_failed';
47
+ hint?: string;
48
+ status?: number;
49
+ };
50
+
51
+ type ProcessHint = {
52
+ pid: number;
53
+ directory: string;
54
+ projectName: string;
55
+ reason: 'process_without_api_port';
56
+ };
57
+
58
+ type SessionSnapshot = {
59
+ savedAt: number;
60
+ sessions: OpencodeSession[];
61
+ processHints: ProcessHint[];
62
+ hostStatuses?: SessionHostStatus[];
63
+ };
64
+
65
+ export type SessionHostStatus = {
66
+ hostId: string;
67
+ hostLabel: string;
68
+ hostKind: 'local' | 'remote';
69
+ online: boolean;
70
+ degraded?: boolean;
71
+ reason?: string;
72
+ baseUrl?: string;
73
+ };
74
+
75
+ type SessionsResponse = {
76
+ sessions: OpencodeSession[];
77
+ processHints?: ProcessHint[];
78
+ failedPorts?: Array<{ port: number; reason: string }>;
79
+ degraded?: boolean;
80
+ hostStatuses?: SessionHostStatus[];
81
+ };
82
+
83
+ type SessionsErrorPayload = {
84
+ error?: string;
85
+ hint?: string;
86
+ degraded?: boolean;
87
+ sessions?: unknown;
88
+ };
89
+
90
+ function areHostStatusesEqual(
91
+ previous: SessionHostStatus[] | null,
92
+ next: SessionHostStatus[]
93
+ ): boolean {
94
+ if (!previous) {
95
+ return false;
96
+ }
97
+
98
+ if (previous.length !== next.length) {
99
+ return false;
100
+ }
101
+
102
+ for (let index = 0; index < previous.length; index += 1) {
103
+ const left = previous[index];
104
+ const right = next[index];
105
+ if (
106
+ left.hostId !== right.hostId ||
107
+ left.hostLabel !== right.hostLabel ||
108
+ left.hostKind !== right.hostKind ||
109
+ left.online !== right.online ||
110
+ left.degraded !== right.degraded ||
111
+ left.reason !== right.reason
112
+ ) {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ return true;
118
+ }
119
+
120
+ function getLocalWaitingPersistenceKey(
121
+ session: Pick<OpencodeSession, 'id' | 'sourceSessionKey' | 'hostId' | 'hostKind'>
122
+ ): string | null {
123
+ const sourceKey = session.sourceSessionKey || session.id;
124
+ if (session.hostKind === 'local' || session.hostId === 'local' || sourceKey.startsWith('local:')) {
125
+ return sourceKey;
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ function getCanonicalSessionIdentity(
132
+ session: Pick<OpencodeSession, 'id' | 'hostId' | 'rawSessionId' | 'sourceSessionKey'>
133
+ ): string {
134
+ if (session.sourceSessionKey) {
135
+ return session.sourceSessionKey;
136
+ }
137
+
138
+ if (session.hostId && session.rawSessionId) {
139
+ return composeSourceKey(session.hostId, session.rawSessionId);
140
+ }
141
+
142
+ if (session.hostId && !session.id.includes(':')) {
143
+ return composeSourceKey(session.hostId, session.id);
144
+ }
145
+
146
+ if (!session.id.includes(':')) {
147
+ return composeSourceKey('local', session.id);
148
+ }
149
+
150
+ return session.id;
151
+ }
152
+
153
+ export function KanbanBoard({
154
+ filterDays,
155
+ onProcessHintsChange,
156
+ hostSources,
157
+ isNodeMode = false,
158
+ showHostFilter = true,
159
+ onHostStatusesChange,
160
+ }: KanbanBoardProps) {
161
+ const cardStatusStateRef = useRef<Record<string, KanbanColumn>>({});
162
+ const cardStatusInitRef = useRef(false);
163
+ const [copyFeedback, setCopyFeedback] = useState<'idle' | 'copied' | 'failed'>('idle');
164
+ const [staleSnapshot, setStaleSnapshot] = useState<SessionSnapshot | null>(null);
165
+ const { enabledSources, activeFilter, setActiveFilter, filteredHostIds } = hostSources;
166
+ const requestSources = useMemo(() => {
167
+ if (!isNodeMode) {
168
+ return enabledSources;
169
+ }
170
+
171
+ const localSources = enabledSources.filter(
172
+ (source) => source.hostKind === 'local' && source.hostId === 'local'
173
+ );
174
+ return localSources.length > 0 ? localSources : [LOCAL_SOURCE];
175
+ }, [enabledSources, isNodeMode]);
176
+
177
+ const { data: config } = useQuery({
178
+ queryKey: ['opencode-config'],
179
+ queryFn: async () => {
180
+ const res = await fetch('/api/opencode-config');
181
+ if (!res.ok) throw new Error('Failed to fetch config');
182
+ return res.json();
183
+ }
184
+ });
185
+
186
+ const configuredRefreshIntervalMs = config?.vibepulse?.sessionsRefreshIntervalMs;
187
+ const refreshIntervalMs =
188
+ typeof configuredRefreshIntervalMs === 'number' && Number.isFinite(configuredRefreshIntervalMs) && configuredRefreshIntervalMs > 0
189
+ ? configuredRefreshIntervalMs
190
+ : 5000;
191
+
192
+ const { data, isLoading, error, dataUpdatedAt, refetch, isFetching, failureCount } = useQuery<SessionsResponse>({
193
+ queryKey: ['sessions', requestSources, isNodeMode],
194
+ queryFn: async ({ signal }: { signal: AbortSignal }) => {
195
+ try {
196
+ const res = await fetch('/api/sessions', {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify({ sources: requestSources }),
200
+ signal
201
+ });
202
+ if (!res.ok) {
203
+ let payload: SessionsErrorPayload | null = null;
204
+ try {
205
+ payload = await res.json() as SessionsErrorPayload;
206
+ } catch {
207
+ payload = null;
208
+ }
209
+
210
+ const isDegradedPayload =
211
+ !!payload &&
212
+ payload.degraded === true &&
213
+ Array.isArray(payload.sessions);
214
+
215
+ if (isDegradedPayload) {
216
+ return payload as SessionsResponse;
217
+ }
218
+
219
+ const isUnavailable =
220
+ res.status === 503 && payload?.error === 'OpenCode server not found';
221
+ const fetchError = new Error(
222
+ isUnavailable
223
+ ? payload?.error || 'OpenCode server not found'
224
+ : payload?.error || `Failed to load sessions (${res.status})`
225
+ ) as SessionsFetchError;
226
+
227
+ fetchError.kind = isUnavailable ? 'opencode_unavailable' : 'request_failed';
228
+ fetchError.hint = payload?.hint;
229
+ fetchError.status = res.status;
230
+ throw fetchError;
231
+ }
232
+
233
+ return res.json();
234
+ } catch (error) {
235
+ if (error instanceof Error && error.name === 'AbortError') {
236
+ throw error;
237
+ }
238
+
239
+ if (error instanceof Error && (error as SessionsFetchError).kind) {
240
+ throw error;
241
+ }
242
+
243
+ const fetchError = new Error('Unable to connect to session service') as SessionsFetchError;
244
+ fetchError.kind = 'request_failed';
245
+ throw fetchError;
246
+ }
247
+ },
248
+ refetchInterval: (query) => query.state.fetchStatus === 'fetching' ? false : refreshIntervalMs,
249
+ refetchIntervalInBackground: true,
250
+ refetchOnReconnect: true,
251
+ retry: false,
252
+ });
253
+
254
+ const activeError = error as SessionsFetchError | null;
255
+ const hasSessionsResponse = data !== undefined;
256
+
257
+ useEffect(() => {
258
+ if (typeof window === 'undefined') return;
259
+ try {
260
+ const raw = localStorage.getItem(SNAPSHOT_STORAGE_KEY);
261
+ if (!raw) return;
262
+ const parsed = JSON.parse(raw) as SessionSnapshot;
263
+ if (!parsed || !Array.isArray(parsed.sessions) || typeof parsed.savedAt !== 'number') {
264
+ return;
265
+ }
266
+ if (!Array.isArray(parsed.processHints)) {
267
+ parsed.processHints = [];
268
+ }
269
+ if (!Array.isArray(parsed.hostStatuses)) {
270
+ parsed.hostStatuses = [];
271
+ }
272
+ if (parsed.sessions.length === 0) return;
273
+ setStaleSnapshot(parsed);
274
+ } catch {
275
+ setStaleSnapshot(null);
276
+ }
277
+ }, []);
278
+
279
+ useEffect(() => {
280
+ if (typeof window === 'undefined') return;
281
+ if (!data?.sessions || data.sessions.length === 0) return;
282
+
283
+ if (data.degraded && staleSnapshot?.sessions?.length) {
284
+ return;
285
+ }
286
+
287
+ const snapshot: SessionSnapshot = {
288
+ savedAt: Date.now(),
289
+ sessions: data.sessions,
290
+ processHints: data.processHints ?? [],
291
+ hostStatuses: data.hostStatuses ?? [],
292
+ };
293
+
294
+ try {
295
+ localStorage.setItem(SNAPSHOT_STORAGE_KEY, JSON.stringify(snapshot));
296
+ setStaleSnapshot(snapshot);
297
+ } catch {
298
+ setStaleSnapshot(snapshot);
299
+ }
300
+ }, [data?.degraded, data?.processHints, data?.sessions, data?.hostStatuses, staleSnapshot?.sessions?.length]);
301
+
302
+ const handleCopyStartCommand = async () => {
303
+ try {
304
+ await navigator.clipboard.writeText(START_COMMAND_TEMPLATE);
305
+ setCopyFeedback('copied');
306
+ setTimeout(() => setCopyFeedback('idle'), 1500);
307
+ } catch {
308
+ setCopyFeedback('failed');
309
+ setTimeout(() => setCopyFeedback('idle'), 2000);
310
+ }
311
+ };
312
+
313
+ const sourceSessions = useMemo(() => {
314
+ if (data?.sessions) {
315
+ if (data.degraded && staleSnapshot?.sessions?.length) {
316
+ const snapshotAgeMs = Date.now() - staleSnapshot.savedAt;
317
+ if (snapshotAgeMs <= DEGRADED_MERGE_MAX_SNAPSHOT_AGE_MS) {
318
+ const merged = [...data.sessions];
319
+ const seen = new Set(merged.map((session) => getCanonicalSessionIdentity(session)));
320
+ for (const session of staleSnapshot.sessions) {
321
+ const canonicalIdentity = getCanonicalSessionIdentity(session);
322
+ if (seen.has(canonicalIdentity)) continue;
323
+ merged.push(session);
324
+ seen.add(canonicalIdentity);
325
+ }
326
+ return merged;
327
+ }
328
+ }
329
+ return data.sessions;
330
+ }
331
+ if (activeError && staleSnapshot?.sessions?.length) {
332
+ return staleSnapshot.sessions;
333
+ }
334
+ return [];
335
+ }, [activeError, data?.degraded, data?.sessions, staleSnapshot?.savedAt, staleSnapshot?.sessions]);
336
+
337
+ const isShowingStaleData = !!activeError && !data?.sessions && !!staleSnapshot?.sessions?.length;
338
+
339
+ const currentHostStatuses = useMemo(() => {
340
+ if (data?.hostStatuses?.length) {
341
+ return data.hostStatuses;
342
+ }
343
+ if (isShowingStaleData && staleSnapshot?.hostStatuses?.length) {
344
+ return staleSnapshot.hostStatuses;
345
+ }
346
+
347
+ if (
348
+ data &&
349
+ !activeError &&
350
+ requestSources.length === 1 &&
351
+ requestSources[0].hostId === 'local' &&
352
+ requestSources[0].hostKind === 'local'
353
+ ) {
354
+ return [{
355
+ hostId: 'local',
356
+ hostLabel: 'Local',
357
+ hostKind: 'local' as const,
358
+ online: true,
359
+ }];
360
+ }
361
+
362
+ return [];
363
+ }, [activeError, data, data?.hostStatuses, requestSources, isShowingStaleData, staleSnapshot?.hostStatuses]);
364
+
365
+ const previousHostStatusesRef = useRef<SessionHostStatus[] | null>(null);
366
+
367
+ useEffect(() => {
368
+ if (!onHostStatusesChange) {
369
+ return;
370
+ }
371
+
372
+ if (areHostStatusesEqual(previousHostStatusesRef.current, currentHostStatuses)) {
373
+ return;
374
+ }
375
+
376
+ previousHostStatusesRef.current = currentHostStatuses.map((status) => ({ ...status }));
377
+ onHostStatusesChange(currentHostStatuses);
378
+ }, [currentHostStatuses, onHostStatusesChange]);
379
+
380
+ const shouldShowHardError =
381
+ !!activeError &&
382
+ !isShowingStaleData &&
383
+ !hasSessionsResponse &&
384
+ failureCount >= SESSIONS_ERROR_DISPLAY_THRESHOLD;
385
+ const shouldShowTransientRecovery =
386
+ !!activeError &&
387
+ !isShowingStaleData &&
388
+ !hasSessionsResponse &&
389
+ failureCount > 0 &&
390
+ failureCount < SESSIONS_ERROR_DISPLAY_THRESHOLD;
391
+
392
+ const processHints = useMemo(() => {
393
+ if (data?.processHints) {
394
+ return data.processHints;
395
+ }
396
+ if (isShowingStaleData && staleSnapshot?.processHints) {
397
+ return staleSnapshot.processHints;
398
+ }
399
+ return [];
400
+ }, [data?.processHints, isShowingStaleData, staleSnapshot?.processHints]);
401
+
402
+ useEffect(() => {
403
+ onProcessHintsChange?.(processHints);
404
+ }, [onProcessHintsChange, processHints]);
405
+
406
+ const enrichedSessions = useMemo(() => {
407
+ if (!sourceSessions.length) return [];
408
+
409
+ let persistedWaiting: Record<string, boolean> = {};
410
+ if (typeof window !== 'undefined') {
411
+ try {
412
+ persistedWaiting = JSON.parse(localStorage.getItem(WAITING_STORAGE_KEY) || '{}');
413
+ } catch {
414
+ persistedWaiting = {};
415
+ }
416
+ }
417
+
418
+ const sseSnapshot = getSseStatusSnapshot();
419
+ const now = Date.now();
420
+ const isPersistedWaitingStillValid = (
421
+ status: string | undefined,
422
+ updatedAt: number,
423
+ persisted: boolean
424
+ ) => {
425
+ if (!persisted) return false;
426
+ if (status !== 'busy' && status !== 'retry') return false;
427
+ if (!updatedAt) return false;
428
+ return now - updatedAt <= WAITING_PERSIST_MAX_AGE_MS;
429
+ };
430
+
431
+ return sourceSessions.map((s) => {
432
+ const waitingPersistenceKey = getLocalWaitingPersistenceKey(s);
433
+ const persisted = waitingPersistenceKey ? !!persistedWaiting[waitingPersistenceKey] : false;
434
+ const updatedAt = s.time?.updated || s.time?.created || 0;
435
+ const keepWaitingFromPersistence = waitingPersistenceKey
436
+ ? isPersistedWaitingStillValid(s.realTimeStatus, updatedAt, persisted)
437
+ : false;
438
+
439
+ const sseEntry = waitingPersistenceKey ? sseSnapshot.get(waitingPersistenceKey) : undefined;
440
+ const sessionStatus = sseEntry ? sseEntry.status : s.realTimeStatus;
441
+
442
+ const children = (s.children || []).map((child) => {
443
+ const childWaitingPersistenceKey = getLocalWaitingPersistenceKey(child);
444
+ const childPersisted = childWaitingPersistenceKey ? !!persistedWaiting[childWaitingPersistenceKey] : false;
445
+ const childUpdatedAt = child.time?.updated || child.time?.created || 0;
446
+ const childSseEntry = childWaitingPersistenceKey ? sseSnapshot.get(childWaitingPersistenceKey) : undefined;
447
+ const childStatus = childSseEntry ? childSseEntry.status : child.realTimeStatus;
448
+ const keepChildWaitingFromPersistence = childWaitingPersistenceKey
449
+ ? isPersistedWaitingStillValid(
450
+ childStatus,
451
+ childUpdatedAt,
452
+ childPersisted
453
+ )
454
+ : false;
455
+
456
+ return {
457
+ ...child,
458
+ realTimeStatus: childStatus,
459
+ waitingForUser: !!child.waitingForUser || keepChildWaitingFromPersistence,
460
+ };
461
+ });
462
+
463
+ return {
464
+ ...s,
465
+ realTimeStatus: sessionStatus,
466
+ waitingForUser: !!s.waitingForUser || keepWaitingFromPersistence,
467
+ children,
468
+ };
469
+ });
470
+ }, [sourceSessions]);
471
+
472
+ useEffect(() => {
473
+ if (typeof window === 'undefined') return;
474
+ const nextPersistedWaiting: Record<string, boolean> = {};
475
+ for (const session of enrichedSessions as Array<{ id: string; waitingForUser?: boolean; children?: Array<{ id: string; waitingForUser?: boolean }> }>) {
476
+ const sessionKey = getLocalWaitingPersistenceKey(session);
477
+ if (session.waitingForUser && sessionKey) {
478
+ nextPersistedWaiting[sessionKey] = true;
479
+ }
480
+
481
+ for (const child of session.children || []) {
482
+ const childKey = getLocalWaitingPersistenceKey(child);
483
+ if (child.waitingForUser && childKey) {
484
+ nextPersistedWaiting[childKey] = true;
485
+ }
486
+ }
487
+ }
488
+ localStorage.setItem(WAITING_STORAGE_KEY, JSON.stringify(nextPersistedWaiting));
489
+ }, [enrichedSessions]);
490
+
491
+ const cards: KanbanCard[] = useMemo(() => {
492
+ const allCards = transformSessions(enrichedSessions);
493
+ let filtered = allCards;
494
+ if (filteredHostIds) {
495
+ filtered = filtered.filter(card => {
496
+ const cardHostId = card.hostId || 'local';
497
+ return filteredHostIds.has(cardHostId);
498
+ });
499
+ }
500
+
501
+ if (filterDays === 0) {
502
+ return filtered;
503
+ }
504
+ const cutoff = dataUpdatedAt - filterDays * 24 * 60 * 60 * 1000;
505
+
506
+ return filtered.filter((card) => {
507
+ if (card.status === 'busy' || card.status === 'review') {
508
+ return true;
509
+ }
510
+
511
+ const lastActivityAt = Math.max(
512
+ card.updatedAt || 0,
513
+ card.createdAt || 0,
514
+ card.archivedAt || 0
515
+ );
516
+ return lastActivityAt >= cutoff;
517
+ });
518
+ }, [dataUpdatedAt, enrichedSessions, filterDays, filteredHostIds]);
519
+
520
+ useEffect(() => {
521
+ const nextCardStatus: Record<string, KanbanColumn> = {};
522
+ for (const card of cards) {
523
+ nextCardStatus[card.id] = card.status;
524
+ }
525
+
526
+ if (!cardStatusInitRef.current) {
527
+ cardStatusInitRef.current = true;
528
+ cardStatusStateRef.current = nextCardStatus;
529
+ return;
530
+ }
531
+
532
+ const shouldPlayComplete = Object.entries(nextCardStatus).some(([id, currentStatus]) => {
533
+ const previousStatus = cardStatusStateRef.current[id];
534
+ return !!previousStatus && previousStatus !== 'idle' && currentStatus === 'idle';
535
+ });
536
+
537
+ cardStatusStateRef.current = nextCardStatus;
538
+
539
+ if (shouldPlayComplete && !isShowingStaleData) {
540
+ setTimeout(() => playCompleteSound(), CARD_ANIMATION_DURATION_MS);
541
+ }
542
+ }, [cards, isShowingStaleData]);
543
+
544
+
545
+ const renderHostFilter = () => (
546
+ <div className="shrink-0 flex items-center justify-end px-4 py-2 bg-zinc-50 dark:bg-black border-b border-gray-200 dark:border-zinc-800">
547
+ <div className="flex items-center gap-1 bg-gray-100 dark:bg-zinc-800 rounded-lg p-0.5" data-testid="host-filter">
548
+ <button
549
+ type="button"
550
+ onClick={() => setActiveFilter('all')}
551
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-all duration-150 ${
552
+ activeFilter === 'all'
553
+ ? 'bg-white dark:bg-zinc-600 text-gray-900 dark:text-white shadow-sm'
554
+ : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
555
+ }`}
556
+ data-testid="host-filter-option-all"
557
+ >
558
+ All Hosts
559
+ </button>
560
+ {enabledSources.map(source => {
561
+ const status = currentHostStatuses.find((s: SessionHostStatus) => s.hostId === source.hostId);
562
+ const isOnline = status?.online ?? false;
563
+ const hostAccentClass = getHostAccentTextClass(source.hostId, source.hostLabel);
564
+
565
+ return (
566
+ <button
567
+ key={source.hostId}
568
+ type="button"
569
+ onClick={() => setActiveFilter(source.hostId)}
570
+ className={`flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-md transition-all duration-150 ${
571
+ activeFilter === source.hostId
572
+ ? 'bg-white dark:bg-zinc-600 text-gray-900 dark:text-white shadow-sm'
573
+ : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
574
+ }`}
575
+ data-testid={`host-filter-option-${source.hostId}`}
576
+ >
577
+ <span className={`inline-flex items-center justify-center flex-shrink-0 ${hostAccentClass}`} data-testid={`host-identity-${source.hostId}`} title={`Host identity: ${source.hostLabel}`}>
578
+ <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
579
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
580
+ </svg>
581
+ </span>
582
+ <span className="truncate">{source.hostLabel}</span>
583
+ <span className="ml-auto inline-flex items-center pl-1.5" data-testid={`host-indicators-${source.hostId}`}>
584
+ <span
585
+ className={`w-2 h-2 rounded-full ${isOnline ? 'bg-emerald-500' : 'bg-gray-400'}`}
586
+ data-testid={`host-status-${source.hostId}`}
587
+ title={isOnline ? 'Online' : 'Offline'}
588
+ />
589
+ </span>
590
+ </button>
591
+ );
592
+ })}
593
+ </div>
594
+ </div>
595
+ );
596
+
597
+ const hostFilterNode: ReactNode = showHostFilter ? renderHostFilter() : null;
598
+
599
+ if (isLoading) {
600
+ return (
601
+ <div className="flex flex-col flex-1 h-full min-h-0">
602
+ {hostFilterNode}
603
+ <LoadingState />
604
+ </div>
605
+ );
606
+ }
607
+
608
+ if (shouldShowHardError) {
609
+ const isOpencodeUnavailable = activeError?.kind === 'opencode_unavailable';
610
+ const title = isOpencodeUnavailable ? 'OpenCode is not running' : 'Failed to load sessions';
611
+ const description = isOpencodeUnavailable
612
+ ? activeError?.hint || 'Run OpenCode with an exposed API port, for example `opencode --port <PORT>`.'
613
+ : activeError?.message || 'An error occurred while loading sessions';
614
+
615
+ return (
616
+ <div className="flex flex-col flex-1 h-full min-h-0">
617
+ {hostFilterNode}
618
+ <div className="flex-1 flex items-center justify-center p-8">
619
+ <div className="max-w-md w-full text-center">
620
+ <div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-red-100 dark:bg-red-900/30 rounded-full">
621
+ <svg
622
+ className="w-6 h-6 text-red-600 dark:text-red-400"
623
+ fill="none"
624
+ stroke="currentColor"
625
+ viewBox="0 0 24 24"
626
+ aria-hidden="true"
627
+ >
628
+ <path
629
+ strokeLinecap="round"
630
+ strokeLinejoin="round"
631
+ strokeWidth={2}
632
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
633
+ />
634
+ </svg>
635
+ </div>
636
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
637
+ {title}
638
+ </h2>
639
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
640
+ {description}
641
+ </p>
642
+ <div className="flex items-center justify-center gap-2">
643
+ <button
644
+ type="button"
645
+ onClick={() => refetch()}
646
+ className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
647
+ disabled={isFetching}
648
+ >
649
+ {isFetching ? 'Retrying...' : 'Retry'}
650
+ </button>
651
+ {isOpencodeUnavailable ? (
652
+ <button
653
+ type="button"
654
+ onClick={handleCopyStartCommand}
655
+ className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md transition-colors"
656
+ >
657
+ {copyFeedback === 'copied'
658
+ ? 'Copied'
659
+ : copyFeedback === 'failed'
660
+ ? 'Copy Failed'
661
+ : 'Copy Start Command'}
662
+ </button>
663
+ ) : null}
664
+ </div>
665
+ </div>
666
+ </div>
667
+ </div>
668
+ );
669
+ }
670
+
671
+ if (shouldShowTransientRecovery) {
672
+ return (
673
+ <div className="flex flex-col flex-1 h-full min-h-0">
674
+ {hostFilterNode}
675
+ <div className="flex-1 flex items-center justify-center p-8">
676
+ <div className="max-w-md w-full text-center">
677
+ <div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-amber-100 dark:bg-amber-900/30 rounded-full">
678
+ <svg
679
+ className="w-6 h-6 text-amber-600 dark:text-amber-400"
680
+ fill="none"
681
+ stroke="currentColor"
682
+ viewBox="0 0 24 24"
683
+ aria-hidden="true"
684
+ >
685
+ <path
686
+ strokeLinecap="round"
687
+ strokeLinejoin="round"
688
+ strokeWidth={2}
689
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
690
+ />
691
+ </svg>
692
+ </div>
693
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
694
+ Reconnecting to session service...
695
+ </h2>
696
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
697
+ Temporary fetch failure ({failureCount}/{SESSIONS_ERROR_DISPLAY_THRESHOLD}). Retrying automatically.
698
+ </p>
699
+ <button
700
+ type="button"
701
+ onClick={() => refetch()}
702
+ className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
703
+ disabled={isFetching}
704
+ >
705
+ {isFetching ? 'Retrying...' : 'Retry now'}
706
+ </button>
707
+ </div>
708
+ </div>
709
+ </div>
710
+ );
711
+ }
712
+
713
+ if (!cards || cards.length === 0) {
714
+ return (
715
+ <div className="flex flex-col flex-1 h-full min-h-0">
716
+ {hostFilterNode}
717
+ <div className="flex-1 flex items-center justify-center p-8">
718
+ <div className="max-w-md w-full text-center">
719
+ <div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-gray-100 dark:bg-zinc-800 rounded-full">
720
+ <svg
721
+ className="w-6 h-6 text-gray-500 dark:text-gray-400"
722
+ fill="none"
723
+ stroke="currentColor"
724
+ viewBox="0 0 24 24"
725
+ aria-hidden="true"
726
+ >
727
+ <path
728
+ strokeLinecap="round"
729
+ strokeLinejoin="round"
730
+ strokeWidth={2}
731
+ d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
732
+ />
733
+ </svg>
734
+ </div>
735
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
736
+ No sessions yet
737
+ </h2>
738
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
739
+ OpenCode is running, but no sessions are available.
740
+ </p>
741
+ <p className="text-sm text-gray-500 dark:text-gray-500">
742
+ Start a conversation in OpenCode and this board will update automatically.
743
+ </p>
744
+ </div>
745
+ </div>
746
+ </div>
747
+ );
748
+ }
749
+
750
+ // Group cards by project
751
+ const groupByProject = (columnCards: KanbanCard[]) => {
752
+ const groups = new Map<string, {
753
+ projectName: string;
754
+ branch?: string;
755
+ hostLabel?: string;
756
+ readOnly: boolean;
757
+ cards: KanbanCard[];
758
+ }>();
759
+
760
+ for (const card of columnCards) {
761
+ const projectName = card.projectName || 'Unknown Project';
762
+ const hostId = card.hostId || 'local';
763
+ const key = `${hostId}::${projectName}`;
764
+
765
+ if (!groups.has(key)) {
766
+ groups.set(key, {
767
+ projectName,
768
+ branch: card.branch,
769
+ hostLabel: card.hostLabel,
770
+ readOnly: !!card.readOnly,
771
+ cards: [],
772
+ });
773
+ }
774
+
775
+ const group = groups.get(key)!;
776
+ group.cards.push(card);
777
+ group.readOnly = group.readOnly || !!card.readOnly;
778
+
779
+ if (!group.branch && card.branch) {
780
+ group.branch = card.branch;
781
+ }
782
+
783
+ if (!group.hostLabel && card.hostLabel) {
784
+ group.hostLabel = card.hostLabel;
785
+ }
786
+ }
787
+
788
+ return groups;
789
+ };
790
+
791
+ return (
792
+ <div className="flex flex-col flex-1 h-full min-h-0">
793
+ {hostFilterNode}
794
+ <div className="flex-1 overflow-x-auto scrollbar-thin scroll-smooth relative">
795
+ {isShowingStaleData ? (
796
+ <div className="px-4 pt-4 pb-0">
797
+ <div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-amber-800 dark:border-amber-900/40 dark:bg-amber-900/20 dark:text-amber-200">
798
+ <div className="flex items-center gap-2 text-xs font-medium">
799
+ <span className="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[10px] uppercase tracking-wide dark:bg-amber-900/40">
800
+ Stale Data
801
+ </span>
802
+ <span>
803
+ Last seen at {staleSnapshot ? new Date(staleSnapshot.savedAt).toLocaleString() : '--'}
804
+ </span>
805
+ <span className="text-amber-700/80 dark:text-amber-300/80">Read-only snapshot while OpenCode is unreachable.</span>
806
+ </div>
807
+ </div>
808
+ </div>
809
+ ) : null}
810
+ <div className="flex gap-6 h-full min-w-max p-4">
811
+ {COLUMNS.map((column) => {
812
+ const columnCards = cards
813
+ .filter((c) => c.status === column.id)
814
+ .sort((a, b) => a.sortOrder - b.sortOrder);
815
+ const projectGroups = groupByProject(columnCards);
816
+
817
+ return (
818
+ <div
819
+ key={column.id}
820
+ className="flex-shrink-0 w-80 bg-gray-100 dark:bg-zinc-800/80 rounded-xl p-4 flex flex-col shadow-sm"
821
+ >
822
+ <div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-200 dark:border-zinc-700">
823
+ <h2 className="font-semibold text-gray-700 dark:text-gray-300">
824
+ {column.title}
825
+ </h2>
826
+ <span className="px-2.5 py-0.5 bg-gray-200 dark:bg-zinc-700 text-gray-600 dark:text-gray-400 text-xs font-medium rounded-full">
827
+ {columnCards.length}
828
+ </span>
829
+ </div>
830
+ <div className="flex-1 overflow-y-auto scrollbar-thin pr-1">
831
+ <div className="space-y-3">
832
+ {Array.from(projectGroups.entries()).map(([groupKey, group]) => (
833
+ <ProjectCard
834
+ key={groupKey}
835
+ projectName={group.projectName}
836
+ branch={group.branch}
837
+ cards={group.cards}
838
+ readOnly={isShowingStaleData || group.readOnly}
839
+ hostLabel={group.hostLabel}
840
+ multipleHostsEnabled={requestSources.length > 1}
841
+ />
842
+ ))}
843
+ </div>
844
+ </div>
845
+ </div>
846
+ );
847
+ })}
848
+ </div>
849
+ </div>
850
+ </div>
851
+ );
852
+ }