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,1298 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('@opencode-ai/sdk', () => ({
4
+ createOpencodeClient: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('@/lib/opencodeDiscovery', () => ({
8
+ discoverOpencodePortsWithMeta: vi.fn(),
9
+ discoverOpencodeProcessCwdsWithoutPortWithMeta: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('@/lib/opencodeConfig', () => ({
13
+ readConfig: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('@/lib/nodeRegistry', () => ({
17
+ listNodeRecords: vi.fn(),
18
+ }));
19
+
20
+ vi.mock('child_process', async () => {
21
+ const execSync = vi.fn();
22
+ return {
23
+ execSync,
24
+ default: {
25
+ execSync,
26
+ },
27
+ };
28
+ });
29
+
30
+ vi.mock('@/lib/sessionArchiveOverrides', () => ({
31
+ clearSessionForceUnarchived: vi.fn(),
32
+ markSessionForceUnarchived: vi.fn(),
33
+ pruneSessionStickyStatusBlocked: vi.fn(),
34
+ pruneSessionForceUnarchived: vi.fn(),
35
+ shouldForceSessionUnarchived: vi.fn(() => false),
36
+ takeSessionStickyStatusBlocked: vi.fn(() => false),
37
+ }));
38
+
39
+ import { createOpencodeClient } from '@opencode-ai/sdk';
40
+ import { execSync } from 'child_process';
41
+ import {
42
+ discoverOpencodePortsWithMeta,
43
+ discoverOpencodeProcessCwdsWithoutPortWithMeta,
44
+ } from '@/lib/opencodeDiscovery';
45
+ import { readConfig } from '@/lib/opencodeConfig';
46
+ import { listNodeRecords } from '@/lib/nodeRegistry';
47
+ import { NODE_PROTOCOL_VERSION } from '@/lib/nodeProtocol';
48
+
49
+ import {
50
+ GET,
51
+ POST,
52
+ applyStickyBusyStatus,
53
+ applyStickyStatusStabilization,
54
+ shouldSkipSessionStatusStabilization,
55
+ } from './route';
56
+
57
+ const mockSessionList: any = vi.fn();
58
+ const mockSessionStatus: any = vi.fn();
59
+ const mockSessionMessages: any = vi.fn();
60
+ const mockCreateOpencodeClient: any = createOpencodeClient;
61
+ const mockDiscoverPortsWithMeta: any = discoverOpencodePortsWithMeta;
62
+ const mockDiscoverProcessCwdsWithoutPortWithMeta: any = discoverOpencodeProcessCwdsWithoutPortWithMeta;
63
+ const mockReadConfig: any = readConfig;
64
+ const mockListNodeRecords: any = listNodeRecords;
65
+ const mockExecSync: any = execSync;
66
+
67
+ function resetDefaultClientMock(): void {
68
+ mockCreateOpencodeClient.mockImplementation(() => ({
69
+ session: {
70
+ list: mockSessionList,
71
+ status: mockSessionStatus,
72
+ messages: mockSessionMessages,
73
+ },
74
+ }) as never);
75
+ }
76
+
77
+ function createNeverResolvingPromise<T>(signal?: AbortSignal): Promise<T> {
78
+ return new Promise<T>((resolve, reject) => {
79
+ if (!signal) {
80
+ return;
81
+ }
82
+
83
+ signal.addEventListener(
84
+ 'abort',
85
+ () => {
86
+ reject(new Error('aborted'));
87
+ },
88
+ { once: true }
89
+ );
90
+
91
+ void resolve;
92
+ });
93
+ }
94
+
95
+ resetDefaultClientMock();
96
+
97
+ type TestSession = {
98
+ id: string;
99
+ time?: {
100
+ archived?: number;
101
+ };
102
+ realTimeStatus: 'idle' | 'busy' | 'retry';
103
+ waitingForUser: boolean;
104
+ children: Array<{
105
+ id: string;
106
+ time?: {
107
+ archived?: number;
108
+ };
109
+ realTimeStatus: 'idle' | 'busy' | 'retry';
110
+ waitingForUser: boolean;
111
+ }>;
112
+ };
113
+
114
+ function setupLocalSessionsMocks(): void {
115
+ resetDefaultClientMock();
116
+
117
+ mockReadConfig.mockResolvedValue({
118
+ vibepulse: {
119
+ stickyBusyDelayMs: 1000,
120
+ },
121
+ });
122
+
123
+ mockDiscoverProcessCwdsWithoutPortWithMeta.mockReturnValue({
124
+ processes: [{ pid: 321, cwd: '/repo/orphan-project' }],
125
+ timedOut: false,
126
+ });
127
+
128
+ mockDiscoverPortsWithMeta.mockReturnValue({
129
+ ports: [7777],
130
+ timedOut: false,
131
+ });
132
+
133
+ mockSessionList.mockResolvedValue({
134
+ data: [
135
+ {
136
+ id: 'parent-1',
137
+ slug: 'parent-1',
138
+ title: 'Parent Session',
139
+ directory: '/repo/project-one',
140
+ time: { created: 1_000, updated: Date.now() - 5_000 },
141
+ },
142
+ {
143
+ id: 'child-1',
144
+ title: 'Child Session',
145
+ directory: '/repo/project-one',
146
+ parentID: 'parent-1',
147
+ time: { created: 1_100, updated: Date.now() - 3_000 },
148
+ },
149
+ ],
150
+ });
151
+
152
+ mockSessionStatus.mockResolvedValue({
153
+ data: {
154
+ 'parent-1': { type: 'busy' },
155
+ },
156
+ });
157
+
158
+ mockSessionMessages.mockImplementation(({ path }: { path: { id: string } }) => {
159
+ if (path.id === 'child-1') {
160
+ return Promise.resolve({
161
+ data: [
162
+ {
163
+ parts: [{ state: { status: 'awaiting-input' } }],
164
+ },
165
+ ],
166
+ });
167
+ }
168
+
169
+ return Promise.resolve({
170
+ data: [
171
+ {
172
+ parts: [{ state: { status: 'running' } }],
173
+ },
174
+ ],
175
+ });
176
+ });
177
+
178
+ mockExecSync.mockImplementation((command: string) => {
179
+ if (command === 'git rev-parse --is-inside-work-tree') {
180
+ return 'true\n';
181
+ }
182
+
183
+ if (command === 'git branch --show-current') {
184
+ return 'main\n';
185
+ }
186
+
187
+ throw new Error(`Unexpected command: ${command}`);
188
+ });
189
+ }
190
+
191
+ afterEach(() => {
192
+ vi.clearAllMocks();
193
+ resetDefaultClientMock();
194
+ vi.unstubAllGlobals();
195
+ });
196
+
197
+ describe('/api/sessions status stabilization ordering', () => {
198
+ it('keeps archived idle session from being re-marked busy by sticky fallback', () => {
199
+ const now = 50_000;
200
+ const stickyBusyDelayMs = 1_000;
201
+ const sessionId = `archived-idle-${Date.now()}-${Math.random()}`;
202
+
203
+ applyStickyBusyStatus(sessionId, 'busy', now - 200, stickyBusyDelayMs);
204
+
205
+ const session: TestSession = {
206
+ id: sessionId,
207
+ time: { archived: now - 100 },
208
+ realTimeStatus: 'idle',
209
+ waitingForUser: false,
210
+ children: [],
211
+ };
212
+
213
+ const skipped = shouldSkipSessionStatusStabilization(session, now);
214
+ expect(skipped).toBe(true);
215
+
216
+ applyStickyStatusStabilization(session, now, stickyBusyDelayMs);
217
+ expect(session.realTimeStatus).toBe('idle');
218
+ });
219
+
220
+ it('still applies sticky busy for active unarchived sessions', () => {
221
+ const now = 80_000;
222
+ const stickyBusyDelayMs = 1_000;
223
+ const sessionId = `active-${Date.now()}-${Math.random()}`;
224
+
225
+ applyStickyBusyStatus(sessionId, 'busy', now - 150, stickyBusyDelayMs);
226
+
227
+ const session: TestSession = {
228
+ id: sessionId,
229
+ realTimeStatus: 'idle',
230
+ waitingForUser: false,
231
+ children: [],
232
+ };
233
+
234
+ const skipped = shouldSkipSessionStatusStabilization(session, now);
235
+ expect(skipped).toBe(false);
236
+
237
+ applyStickyStatusStabilization(session, now, stickyBusyDelayMs);
238
+ expect(session.realTimeStatus).toBe('busy');
239
+ });
240
+
241
+ it('skips sticky stabilization for archived children under active parent', () => {
242
+ const now = 120_000;
243
+ const stickyBusyDelayMs = 1_000;
244
+ const childId = `archived-child-${Date.now()}-${Math.random()}`;
245
+
246
+ applyStickyBusyStatus(`child:${childId}`, 'busy', now - 100, stickyBusyDelayMs);
247
+
248
+ const session: TestSession = {
249
+ id: `parent-${Date.now()}-${Math.random()}`,
250
+ realTimeStatus: 'idle',
251
+ waitingForUser: false,
252
+ children: [
253
+ {
254
+ id: childId,
255
+ time: { archived: now - 50 },
256
+ realTimeStatus: 'idle',
257
+ waitingForUser: false,
258
+ },
259
+ ],
260
+ };
261
+
262
+ applyStickyStatusStabilization(session, now, stickyBusyDelayMs);
263
+
264
+ expect(session.children[0].realTimeStatus).toBe('idle');
265
+ });
266
+ });
267
+
268
+ describe('/api/sessions route source handling', () => {
269
+ const originalRuntimeRole = process.env.VIBEPULSE_RUNTIME_ROLE;
270
+
271
+ beforeEach(() => {
272
+ process.env.VIBEPULSE_RUNTIME_ROLE = 'hub';
273
+ });
274
+
275
+ afterEach(() => {
276
+ process.env.VIBEPULSE_RUNTIME_ROLE = originalRuntimeRole;
277
+ });
278
+
279
+ it('enforces local-only aggregation in node mode even when remote sources are requested', async () => {
280
+ process.env.VIBEPULSE_RUNTIME_ROLE = 'node';
281
+ setupLocalSessionsMocks();
282
+ mockListNodeRecords.mockResolvedValue([
283
+ {
284
+ nodeId: 'remote-a',
285
+ nodeLabel: 'Remote A',
286
+ baseUrl: 'https://remote-a.test',
287
+ enabled: true,
288
+ token: 'token-a',
289
+ createdAt: new Date().toISOString(),
290
+ updatedAt: new Date().toISOString(),
291
+ },
292
+ ]);
293
+
294
+ const mockFetch: any = vi.fn();
295
+ vi.stubGlobal('fetch', mockFetch);
296
+
297
+ const response = await POST(
298
+ new Request('http://localhost/api/sessions', {
299
+ method: 'POST',
300
+ headers: { 'content-type': 'application/json' },
301
+ body: JSON.stringify({
302
+ sources: [
303
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
304
+ {
305
+ hostId: 'remote-a',
306
+ hostLabel: 'Remote A',
307
+ hostKind: 'remote',
308
+ baseUrl: 'https://remote-a.test',
309
+ enabled: true,
310
+ },
311
+ ],
312
+ }),
313
+ })
314
+ );
315
+ const data = await response.json();
316
+
317
+ expect(response.status).toBe(200);
318
+ expect(data.hostStatuses).toEqual([
319
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true },
320
+ ]);
321
+ expect(data.hosts).toEqual(data.hostStatuses);
322
+ expect(data.sessions.every((session: any) => session.hostId === 'local')).toBe(true);
323
+ expect(mockFetch.mock.calls).toHaveLength(0);
324
+ expect(mockListNodeRecords.mock.calls).toHaveLength(0);
325
+ });
326
+
327
+ it('keeps GET local aggregation behavior working without request host config', async () => {
328
+ setupLocalSessionsMocks();
329
+
330
+ const response = await GET();
331
+ const data = await response.json();
332
+
333
+ expect(response.status).toBe(200);
334
+ expect(data.processHints).toEqual([
335
+ {
336
+ pid: 321,
337
+ directory: '/repo/orphan-project',
338
+ projectName: 'orphan-project',
339
+ reason: 'process_without_api_port',
340
+ },
341
+ ]);
342
+ expect(data.sessions).toHaveLength(1);
343
+
344
+ const session = data.sessions[0];
345
+ expect(session.id).toBe('parent-1');
346
+ expect(session.slug).toBe('parent-1');
347
+ expect(session.title).toBe('Parent Session');
348
+ expect(session.directory).toBe('/repo/project-one');
349
+ expect(session.time.created).toBe(1_000);
350
+ expect(typeof session.time.updated).toBe('number');
351
+ expect(session.projectName).toBe('project-one');
352
+ expect(session.branch).toBe('main');
353
+ expect(session.realTimeStatus).toBe('busy');
354
+ expect(session.waitingForUser).toBe(false);
355
+ expect(session.children).toHaveLength(1);
356
+
357
+ const child = session.children[0];
358
+ expect(child.id).toBe('child-1');
359
+ expect(child.title).toBe('Child Session');
360
+ expect(child.directory).toBe('/repo/project-one');
361
+ expect(child.parentID).toBe('parent-1');
362
+ expect(child.time?.created).toBe(1_100);
363
+ expect(typeof child.time?.updated).toBe('number');
364
+ expect(child.realTimeStatus).toBe('busy');
365
+ expect(child.waitingForUser).toBe(true);
366
+ });
367
+
368
+ it('returns host-aware Local identities for POST when only the Local source is requested', async () => {
369
+ setupLocalSessionsMocks();
370
+
371
+ const getResponse = await GET();
372
+ const getData = await getResponse.json();
373
+
374
+ const postResponse = await POST(
375
+ new Request('http://localhost/api/sessions', {
376
+ method: 'POST',
377
+ headers: { 'content-type': 'application/json' },
378
+ body: JSON.stringify({
379
+ sources: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local' }],
380
+ }),
381
+ })
382
+ );
383
+ const postData = await postResponse.json();
384
+
385
+ expect(getResponse.status).toBe(200);
386
+ expect(postResponse.status).toBe(200);
387
+ expect(postData.sessions).toHaveLength(1);
388
+ expect(postData.processHints).toEqual(getData.processHints);
389
+ expect(postData.hostStatuses).toEqual([
390
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true },
391
+ ]);
392
+ expect(postData.hosts).toEqual(postData.hostStatuses);
393
+
394
+ expect(postData.sessions[0]).toMatchObject({
395
+ id: 'local:parent-1',
396
+ slug: getData.sessions[0].slug,
397
+ title: getData.sessions[0].title,
398
+ directory: getData.sessions[0].directory,
399
+ time: getData.sessions[0].time,
400
+ projectName: getData.sessions[0].projectName,
401
+ branch: getData.sessions[0].branch,
402
+ realTimeStatus: getData.sessions[0].realTimeStatus,
403
+ waitingForUser: getData.sessions[0].waitingForUser,
404
+ rawSessionId: 'parent-1',
405
+ sourceSessionKey: 'local:parent-1',
406
+ hostId: 'local',
407
+ hostLabel: 'Local',
408
+ hostKind: 'local',
409
+ readOnly: false,
410
+ });
411
+ expect(postData.sessions[0].children).toHaveLength(1);
412
+ expect(postData.sessions[0].children[0]).toMatchObject({
413
+ ...getData.sessions[0].children[0],
414
+ id: 'local:child-1',
415
+ parentID: 'local:parent-1',
416
+ rawSessionId: 'child-1',
417
+ sourceSessionKey: 'local:child-1',
418
+ hostId: 'local',
419
+ hostLabel: 'Local',
420
+ hostKind: 'local',
421
+ readOnly: false,
422
+ });
423
+ });
424
+
425
+ it('keeps GET offline behavior but returns a degraded error payload for local-only POST when Local is offline', async () => {
426
+ mockReadConfig.mockResolvedValue({
427
+ vibepulse: {
428
+ stickyBusyDelayMs: 1000,
429
+ },
430
+ });
431
+ mockDiscoverProcessCwdsWithoutPortWithMeta.mockReturnValue({
432
+ processes: [{ pid: 654, cwd: '/repo/offline-local-project' }],
433
+ timedOut: false,
434
+ });
435
+ mockDiscoverPortsWithMeta.mockReturnValue({
436
+ ports: [],
437
+ timedOut: false,
438
+ });
439
+
440
+ const getResponse = await GET();
441
+ const getData = await getResponse.json();
442
+
443
+ const postResponse = await POST(
444
+ new Request('http://localhost/api/sessions', {
445
+ method: 'POST',
446
+ headers: { 'content-type': 'application/json' },
447
+ body: JSON.stringify({
448
+ sources: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local' }],
449
+ }),
450
+ })
451
+ );
452
+ const postData = await postResponse.json();
453
+
454
+ expect(getResponse.status).toBe(200);
455
+ expect(getData).toEqual({
456
+ sessions: [],
457
+ processHints: [
458
+ {
459
+ pid: 654,
460
+ directory: '/repo/offline-local-project',
461
+ projectName: 'offline-local-project',
462
+ reason: 'process_without_api_port',
463
+ },
464
+ ],
465
+ });
466
+
467
+ expect(postResponse.status).toBe(503);
468
+ expect(postData).toEqual({
469
+ sessions: [],
470
+ processHints: [
471
+ {
472
+ pid: 654,
473
+ directory: '/repo/offline-local-project',
474
+ projectName: 'offline-local-project',
475
+ reason: 'process_without_api_port',
476
+ },
477
+ ],
478
+ degraded: true,
479
+ hosts: [
480
+ {
481
+ hostId: 'local',
482
+ hostLabel: 'Local',
483
+ hostKind: 'local',
484
+ online: false,
485
+ degraded: true,
486
+ reason: 'OpenCode server not found',
487
+ },
488
+ ],
489
+ hostStatuses: [
490
+ {
491
+ hostId: 'local',
492
+ hostLabel: 'Local',
493
+ hostKind: 'local',
494
+ online: false,
495
+ degraded: true,
496
+ reason: 'OpenCode server not found',
497
+ },
498
+ ],
499
+ });
500
+ });
501
+
502
+ it('returns 400 for malformed POST payloads', async () => {
503
+ const response = await POST(
504
+ new Request('http://localhost/api/sessions', {
505
+ method: 'POST',
506
+ headers: { 'content-type': 'application/json' },
507
+ body: JSON.stringify({ sources: 'not-an-array' }),
508
+ })
509
+ );
510
+ const data = await response.json();
511
+
512
+ expect(response.status).toBe(400);
513
+ expect(data).toEqual({
514
+ error: 'Invalid sources payload',
515
+ hint: 'POST /api/sessions expects a JSON body with a non-empty sources array.',
516
+ });
517
+ });
518
+
519
+ it('isolates remote host failures while returning local and successful remote sessions', async () => {
520
+ setupLocalSessionsMocks();
521
+ mockListNodeRecords.mockResolvedValue([
522
+ {
523
+ nodeId: 'remote-a',
524
+ nodeLabel: 'Remote A',
525
+ baseUrl: 'https://remote-a.test',
526
+ enabled: true,
527
+ token: 'token-a',
528
+ createdAt: new Date().toISOString(),
529
+ updatedAt: new Date().toISOString(),
530
+ },
531
+ {
532
+ nodeId: 'remote-b',
533
+ nodeLabel: 'Remote B',
534
+ baseUrl: 'https://remote-b.test',
535
+ enabled: true,
536
+ token: 'token-b',
537
+ createdAt: new Date().toISOString(),
538
+ updatedAt: new Date().toISOString(),
539
+ },
540
+ ]);
541
+
542
+ const mockFetch = vi.fn(async (input: RequestInfo | URL) => {
543
+ const url = typeof input === 'string' ? input : input.toString();
544
+ if (url === 'https://remote-a.test/api/node/sessions') {
545
+ return new Response(
546
+ JSON.stringify({
547
+ ok: true,
548
+ role: 'node',
549
+ protocolVersion: NODE_PROTOCOL_VERSION,
550
+ source: { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
551
+ upstream: { kind: 'opencode', reachable: true },
552
+ sessions: [
553
+ {
554
+ id: 'local:remote-parent-1',
555
+ rawSessionId: 'remote-parent-1',
556
+ sourceSessionKey: 'local:remote-parent-1',
557
+ title: 'Remote Parent',
558
+ directory: '/remote/project-one',
559
+ projectName: 'project-one',
560
+ branch: null,
561
+ realTimeStatus: 'busy',
562
+ waitingForUser: false,
563
+ time: { created: 2_000, updated: Date.now() - 1_000 },
564
+ children: [
565
+ {
566
+ id: 'local:remote-child-1',
567
+ rawSessionId: 'remote-child-1',
568
+ sourceSessionKey: 'local:remote-child-1',
569
+ parentID: 'local:remote-parent-1',
570
+ title: 'Remote Child',
571
+ directory: '/remote/project-one',
572
+ realTimeStatus: 'busy',
573
+ waitingForUser: false,
574
+ time: { created: 2_100, updated: Date.now() - 900 },
575
+ },
576
+ ],
577
+ },
578
+ ],
579
+ processHints: [],
580
+ hosts: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
581
+ hostStatuses: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
582
+ }),
583
+ { status: 200, headers: { 'content-type': 'application/json' } }
584
+ );
585
+ }
586
+
587
+ if (url === 'https://remote-b.test/api/node/sessions') {
588
+ throw new Error('remote-b offline');
589
+ }
590
+
591
+ throw new Error(`Unexpected node sessions URL: ${url}`);
592
+ });
593
+ vi.stubGlobal('fetch', mockFetch);
594
+
595
+ mockCreateOpencodeClient.mockImplementation(({ baseUrl }: { baseUrl: string }) => {
596
+ if (baseUrl === 'http://localhost:7777') {
597
+ return {
598
+ session: {
599
+ list: mockSessionList,
600
+ status: mockSessionStatus,
601
+ messages: mockSessionMessages,
602
+ },
603
+ } as never;
604
+ }
605
+
606
+ throw new Error(`Unexpected baseUrl: ${baseUrl}`);
607
+ });
608
+
609
+ const response = await POST(
610
+ new Request('http://localhost/api/sessions', {
611
+ method: 'POST',
612
+ headers: { 'content-type': 'application/json' },
613
+ body: JSON.stringify({
614
+ sources: [
615
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
616
+ {
617
+ hostId: 'remote-a',
618
+ hostLabel: 'Remote A',
619
+ hostKind: 'remote',
620
+ baseUrl: 'https://remote-a.test',
621
+ enabled: true,
622
+ },
623
+ {
624
+ hostId: 'remote-b',
625
+ hostLabel: 'Remote B',
626
+ hostKind: 'remote',
627
+ baseUrl: 'https://remote-b.test',
628
+ enabled: true,
629
+ },
630
+ ],
631
+ }),
632
+ })
633
+ );
634
+ const data = await response.json();
635
+
636
+ expect(response.status).toBe(200);
637
+ expect(data.degraded).toBe(true);
638
+ expect(data.hostStatuses).toEqual([
639
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true },
640
+ {
641
+ hostId: 'remote-a',
642
+ hostLabel: 'Remote A',
643
+ hostKind: 'remote',
644
+ online: true,
645
+ baseUrl: 'https://remote-a.test',
646
+ },
647
+ {
648
+ hostId: 'remote-b',
649
+ hostLabel: 'Remote B',
650
+ hostKind: 'remote',
651
+ online: false,
652
+ degraded: true,
653
+ reason: 'remote-b offline',
654
+ baseUrl: 'https://remote-b.test',
655
+ },
656
+ ]);
657
+ expect(data.hosts).toEqual(data.hostStatuses);
658
+
659
+ const localSession = data.sessions.find((session: any) => session.hostId === 'local');
660
+ expect(localSession).toMatchObject({
661
+ id: 'local:parent-1',
662
+ rawSessionId: 'parent-1',
663
+ sourceSessionKey: 'local:parent-1',
664
+ hostId: 'local',
665
+ hostLabel: 'Local',
666
+ hostKind: 'local',
667
+ readOnly: false,
668
+ });
669
+
670
+ const remoteSession = data.sessions.find((session: any) => session.hostId === 'remote-a');
671
+ expect(remoteSession).toMatchObject({
672
+ id: 'remote-a:remote-parent-1',
673
+ rawSessionId: 'remote-parent-1',
674
+ sourceSessionKey: 'remote-a:remote-parent-1',
675
+ hostId: 'remote-a',
676
+ hostLabel: 'Remote A',
677
+ hostKind: 'remote',
678
+ readOnly: false,
679
+ branch: null,
680
+ realTimeStatus: 'busy',
681
+ });
682
+ expect(remoteSession.children).toHaveLength(1);
683
+ expect(remoteSession.children[0]).toMatchObject({
684
+ id: 'remote-a:remote-child-1',
685
+ parentID: 'remote-a:remote-parent-1',
686
+ rawSessionId: 'remote-child-1',
687
+ sourceSessionKey: 'remote-a:remote-child-1',
688
+ hostId: 'remote-a',
689
+ hostLabel: 'Remote A',
690
+ hostKind: 'remote',
691
+ readOnly: false,
692
+ realTimeStatus: 'busy',
693
+ });
694
+ expect(mockCreateOpencodeClient.mock.calls).toEqual([[{ baseUrl: 'http://localhost:7777' }]]);
695
+ });
696
+
697
+ it('returns a degraded 200 payload with host status metadata when all sources are offline', async () => {
698
+ mockReadConfig.mockResolvedValue({
699
+ vibepulse: {
700
+ stickyBusyDelayMs: 1000,
701
+ },
702
+ });
703
+ mockDiscoverProcessCwdsWithoutPortWithMeta.mockReturnValue({
704
+ processes: [],
705
+ timedOut: false,
706
+ });
707
+ mockDiscoverPortsWithMeta.mockReturnValue({
708
+ ports: [],
709
+ timedOut: false,
710
+ });
711
+
712
+ mockListNodeRecords.mockResolvedValue([
713
+ {
714
+ nodeId: 'remote-offline',
715
+ nodeLabel: 'Remote Offline',
716
+ baseUrl: 'https://offline-remote.test',
717
+ enabled: true,
718
+ token: 'offline-token',
719
+ createdAt: new Date().toISOString(),
720
+ updatedAt: new Date().toISOString(),
721
+ },
722
+ ]);
723
+
724
+ const mockFetch = vi.fn(async (input: RequestInfo | URL) => {
725
+ const url = typeof input === 'string' ? input : input.toString();
726
+ if (url === 'https://offline-remote.test/api/node/sessions') {
727
+ throw new Error('remote unavailable');
728
+ }
729
+ throw new Error(`Unexpected node sessions URL: ${url}`);
730
+ });
731
+ vi.stubGlobal('fetch', mockFetch);
732
+
733
+ const response = await POST(
734
+ new Request('http://localhost/api/sessions', {
735
+ method: 'POST',
736
+ headers: { 'content-type': 'application/json' },
737
+ body: JSON.stringify({
738
+ sources: [
739
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
740
+ {
741
+ hostId: 'remote-offline',
742
+ hostLabel: 'Remote Offline',
743
+ hostKind: 'remote',
744
+ baseUrl: 'https://offline-remote.test',
745
+ enabled: true,
746
+ },
747
+ ],
748
+ }),
749
+ })
750
+ );
751
+ const data = await response.json();
752
+
753
+ expect(response.status).toBe(200);
754
+ expect(data).toEqual({
755
+ sessions: [],
756
+ processHints: [],
757
+ hosts: [
758
+ {
759
+ hostId: 'local',
760
+ hostLabel: 'Local',
761
+ hostKind: 'local',
762
+ online: false,
763
+ reason: 'OpenCode server not found',
764
+ },
765
+ {
766
+ hostId: 'remote-offline',
767
+ hostLabel: 'Remote Offline',
768
+ hostKind: 'remote',
769
+ online: false,
770
+ degraded: true,
771
+ reason: 'remote unavailable',
772
+ baseUrl: 'https://offline-remote.test',
773
+ },
774
+ ],
775
+ hostStatuses: [
776
+ {
777
+ hostId: 'local',
778
+ hostLabel: 'Local',
779
+ hostKind: 'local',
780
+ online: false,
781
+ reason: 'OpenCode server not found',
782
+ },
783
+ {
784
+ hostId: 'remote-offline',
785
+ hostLabel: 'Remote Offline',
786
+ hostKind: 'remote',
787
+ online: false,
788
+ degraded: true,
789
+ reason: 'remote unavailable',
790
+ baseUrl: 'https://offline-remote.test',
791
+ },
792
+ ],
793
+ degraded: true,
794
+ });
795
+ });
796
+
797
+ it('degrades malformed remote node success payloads instead of trusting 200 responses', async () => {
798
+ setupLocalSessionsMocks();
799
+ mockListNodeRecords.mockResolvedValue([
800
+ {
801
+ nodeId: 'remote-malformed',
802
+ nodeLabel: 'Remote Malformed',
803
+ baseUrl: 'https://remote-malformed.test',
804
+ enabled: true,
805
+ token: 'malformed-token',
806
+ createdAt: new Date().toISOString(),
807
+ updatedAt: new Date().toISOString(),
808
+ },
809
+ ]);
810
+
811
+ const mockFetch = vi.fn(async (input: RequestInfo | URL) => {
812
+ const url = typeof input === 'string' ? input : input.toString();
813
+ if (url === 'https://remote-malformed.test/api/node/sessions') {
814
+ return new Response(
815
+ JSON.stringify({
816
+ sessions: [{ id: 'missing-envelope-fields' }],
817
+ processHints: [],
818
+ }),
819
+ { status: 200, headers: { 'content-type': 'application/json' } }
820
+ );
821
+ }
822
+ throw new Error(`Unexpected node sessions URL: ${url}`);
823
+ });
824
+ vi.stubGlobal('fetch', mockFetch);
825
+
826
+ const response = await POST(
827
+ new Request('http://localhost/api/sessions', {
828
+ method: 'POST',
829
+ headers: { 'content-type': 'application/json' },
830
+ body: JSON.stringify({
831
+ sources: [
832
+ {
833
+ hostId: 'remote-malformed',
834
+ hostLabel: 'Remote Malformed',
835
+ hostKind: 'remote',
836
+ baseUrl: 'https://remote-malformed.test',
837
+ enabled: true,
838
+ },
839
+ ],
840
+ }),
841
+ })
842
+ );
843
+ const data = await response.json();
844
+
845
+ expect(response.status).toBe(200);
846
+ expect(data).toEqual({
847
+ sessions: [],
848
+ processHints: [],
849
+ hosts: [
850
+ {
851
+ hostId: 'remote-malformed',
852
+ hostLabel: 'Remote Malformed',
853
+ hostKind: 'remote',
854
+ online: true,
855
+ degraded: true,
856
+ reason: 'node_payload_invalid',
857
+ baseUrl: 'https://remote-malformed.test',
858
+ },
859
+ ],
860
+ hostStatuses: [
861
+ {
862
+ hostId: 'remote-malformed',
863
+ hostLabel: 'Remote Malformed',
864
+ hostKind: 'remote',
865
+ online: true,
866
+ degraded: true,
867
+ reason: 'node_payload_invalid',
868
+ baseUrl: 'https://remote-malformed.test',
869
+ },
870
+ ],
871
+ degraded: true,
872
+ });
873
+ });
874
+
875
+ it('degrades and skips malformed remote session ids instead of returning 500', async () => {
876
+ setupLocalSessionsMocks();
877
+ mockListNodeRecords.mockResolvedValue([
878
+ {
879
+ nodeId: 'remote-malformed-session-id',
880
+ nodeLabel: 'Remote Malformed Session Id',
881
+ baseUrl: 'https://remote-malformed-session-id.test',
882
+ enabled: true,
883
+ token: 'malformed-session-id-token',
884
+ createdAt: new Date().toISOString(),
885
+ updatedAt: new Date().toISOString(),
886
+ },
887
+ ]);
888
+
889
+ const mockFetch = vi.fn(async (input: RequestInfo | URL) => {
890
+ const url = typeof input === 'string' ? input : input.toString();
891
+ if (url === 'https://remote-malformed-session-id.test/api/node/sessions') {
892
+ return new Response(
893
+ JSON.stringify({
894
+ ok: true,
895
+ role: 'node',
896
+ protocolVersion: NODE_PROTOCOL_VERSION,
897
+ source: { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
898
+ upstream: { kind: 'opencode', reachable: true },
899
+ sessions: [
900
+ {
901
+ id: 'local:bad:session:id',
902
+ rawSessionId: 'bad:session:id',
903
+ title: 'Malformed Session',
904
+ directory: '/remote/malformed',
905
+ projectName: 'malformed',
906
+ branch: null,
907
+ realTimeStatus: 'idle',
908
+ waitingForUser: false,
909
+ time: { created: 2_000, updated: Date.now() - 800 },
910
+ children: [],
911
+ },
912
+ ],
913
+ processHints: [],
914
+ hosts: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
915
+ hostStatuses: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
916
+ }),
917
+ { status: 200, headers: { 'content-type': 'application/json' } }
918
+ );
919
+ }
920
+ throw new Error(`Unexpected node sessions URL: ${url}`);
921
+ });
922
+ vi.stubGlobal('fetch', mockFetch);
923
+
924
+ const response = await POST(
925
+ new Request('http://localhost/api/sessions', {
926
+ method: 'POST',
927
+ headers: { 'content-type': 'application/json' },
928
+ body: JSON.stringify({
929
+ sources: [
930
+ {
931
+ hostId: 'remote-malformed-session-id',
932
+ hostLabel: 'Remote Malformed Session Id',
933
+ hostKind: 'remote',
934
+ baseUrl: 'https://remote-malformed-session-id.test',
935
+ enabled: true,
936
+ },
937
+ ],
938
+ }),
939
+ })
940
+ );
941
+ const data = await response.json();
942
+
943
+ expect(response.status).toBe(200);
944
+ expect(data).toEqual({
945
+ sessions: [],
946
+ processHints: [],
947
+ hosts: [
948
+ {
949
+ hostId: 'remote-malformed-session-id',
950
+ hostLabel: 'Remote Malformed Session Id',
951
+ hostKind: 'remote',
952
+ online: true,
953
+ degraded: true,
954
+ reason: 'node_payload_invalid_session_id',
955
+ baseUrl: 'https://remote-malformed-session-id.test',
956
+ },
957
+ ],
958
+ hostStatuses: [
959
+ {
960
+ hostId: 'remote-malformed-session-id',
961
+ hostLabel: 'Remote Malformed Session Id',
962
+ hostKind: 'remote',
963
+ online: true,
964
+ degraded: true,
965
+ reason: 'node_payload_invalid_session_id',
966
+ baseUrl: 'https://remote-malformed-session-id.test',
967
+ },
968
+ ],
969
+ degraded: true,
970
+ });
971
+ });
972
+
973
+ it('returns 400 for invalid remote source entries', async () => {
974
+ const invalidRemoteUrlResponse = await POST(
975
+ new Request('http://localhost/api/sessions', {
976
+ method: 'POST',
977
+ headers: { 'content-type': 'application/json' },
978
+ body: JSON.stringify({
979
+ sources: [
980
+ {
981
+ hostId: 'remote-invalid-url',
982
+ hostLabel: 'Remote Invalid URL',
983
+ hostKind: 'remote',
984
+ baseUrl: 'not-a-url',
985
+ enabled: true,
986
+ },
987
+ ],
988
+ }),
989
+ })
990
+ );
991
+ const invalidRemoteUrlData = await invalidRemoteUrlResponse.json();
992
+
993
+ expect(invalidRemoteUrlResponse.status).toBe(400);
994
+ expect(invalidRemoteUrlData).toEqual({
995
+ error: 'Invalid sources payload',
996
+ hint: 'POST /api/sessions expects a JSON body with a non-empty sources array.',
997
+ });
998
+
999
+ const ftpRemoteResponse = await POST(
1000
+ new Request('http://localhost/api/sessions', {
1001
+ method: 'POST',
1002
+ headers: { 'content-type': 'application/json' },
1003
+ body: JSON.stringify({
1004
+ sources: [
1005
+ {
1006
+ hostId: 'remote-ftp',
1007
+ hostLabel: 'Remote FTP',
1008
+ hostKind: 'remote',
1009
+ baseUrl: 'ftp://remote-ftp.test',
1010
+ enabled: true,
1011
+ },
1012
+ ],
1013
+ }),
1014
+ })
1015
+ );
1016
+ const ftpRemoteData = await ftpRemoteResponse.json();
1017
+
1018
+ expect(ftpRemoteResponse.status).toBe(400);
1019
+ expect(ftpRemoteData).toEqual({
1020
+ error: 'Invalid sources payload',
1021
+ hint: 'POST /api/sessions expects a JSON body with a non-empty sources array.',
1022
+ });
1023
+
1024
+ const credentialedRemoteResponse = await POST(
1025
+ new Request('http://localhost/api/sessions', {
1026
+ method: 'POST',
1027
+ headers: { 'content-type': 'application/json' },
1028
+ body: JSON.stringify({
1029
+ sources: [
1030
+ {
1031
+ hostId: 'remote-secret',
1032
+ hostLabel: 'Remote Secret',
1033
+ hostKind: 'remote',
1034
+ baseUrl: 'https://user:pass@remote-secret.test',
1035
+ enabled: true,
1036
+ },
1037
+ ],
1038
+ }),
1039
+ })
1040
+ );
1041
+ const credentialedRemoteData = await credentialedRemoteResponse.json();
1042
+
1043
+ expect(credentialedRemoteResponse.status).toBe(400);
1044
+ expect(credentialedRemoteData).toEqual({
1045
+ error: 'Invalid sources payload',
1046
+ hint: 'POST /api/sessions expects a JSON body with a non-empty sources array.',
1047
+ });
1048
+
1049
+ const invalidRemoteShapeResponse = await POST(
1050
+ new Request('http://localhost/api/sessions', {
1051
+ method: 'POST',
1052
+ headers: { 'content-type': 'application/json' },
1053
+ body: JSON.stringify({
1054
+ sources: [
1055
+ {
1056
+ hostId: 'remote-missing-enabled',
1057
+ hostLabel: 'Remote Missing Enabled',
1058
+ hostKind: 'remote',
1059
+ baseUrl: 'https://remote-shape.test',
1060
+ },
1061
+ ],
1062
+ }),
1063
+ })
1064
+ );
1065
+ const invalidRemoteShapeData = await invalidRemoteShapeResponse.json();
1066
+
1067
+ expect(invalidRemoteShapeResponse.status).toBe(400);
1068
+ expect(invalidRemoteShapeData).toEqual({
1069
+ error: 'Invalid sources payload',
1070
+ hint: 'POST /api/sessions expects a JSON body with a non-empty sources array.',
1071
+ });
1072
+ });
1073
+
1074
+ it('degrades non-local sources when node registry has no matching node instead of using direct remote SDK calls', async () => {
1075
+ mockListNodeRecords.mockResolvedValue([]);
1076
+
1077
+ const response = await POST(
1078
+ new Request('http://localhost/api/sessions', {
1079
+ method: 'POST',
1080
+ headers: { 'content-type': 'application/json' },
1081
+ body: JSON.stringify({
1082
+ sources: [
1083
+ {
1084
+ hostId: 'remote-missing',
1085
+ hostLabel: 'Remote Missing',
1086
+ hostKind: 'remote',
1087
+ baseUrl: 'https://raw-opencode-endpoint.test',
1088
+ enabled: true,
1089
+ },
1090
+ ],
1091
+ }),
1092
+ })
1093
+ );
1094
+ const data = await response.json();
1095
+
1096
+ expect(response.status).toBe(200);
1097
+ expect(data).toEqual({
1098
+ sessions: [],
1099
+ processHints: [],
1100
+ hosts: [
1101
+ {
1102
+ hostId: 'remote-missing',
1103
+ hostLabel: 'Remote Missing',
1104
+ hostKind: 'remote',
1105
+ online: false,
1106
+ degraded: true,
1107
+ reason: 'node_not_configured',
1108
+ baseUrl: 'https://raw-opencode-endpoint.test',
1109
+ },
1110
+ ],
1111
+ hostStatuses: [
1112
+ {
1113
+ hostId: 'remote-missing',
1114
+ hostLabel: 'Remote Missing',
1115
+ hostKind: 'remote',
1116
+ online: false,
1117
+ degraded: true,
1118
+ reason: 'node_not_configured',
1119
+ baseUrl: 'https://raw-opencode-endpoint.test',
1120
+ },
1121
+ ],
1122
+ degraded: true,
1123
+ });
1124
+ expect(mockCreateOpencodeClient.mock.calls).toHaveLength(0);
1125
+ });
1126
+
1127
+ it('aborts hanging local SDK calls when timeout elapses', async () => {
1128
+ const originalListTimeoutEnv = process.env.OPENCODE_SESSIONS_LIST_TIMEOUT_MS;
1129
+ const originalStatusTimeoutEnv = process.env.OPENCODE_SESSIONS_STATUS_TIMEOUT_MS;
1130
+
1131
+ process.env.OPENCODE_SESSIONS_LIST_TIMEOUT_MS = '15';
1132
+ process.env.OPENCODE_SESSIONS_STATUS_TIMEOUT_MS = '15';
1133
+
1134
+ vi.resetModules();
1135
+ const { GET: freshGet } = await import('./route');
1136
+
1137
+ mockReadConfig.mockResolvedValue({ vibepulse: { stickyBusyDelayMs: 1000 } });
1138
+ mockDiscoverProcessCwdsWithoutPortWithMeta.mockReturnValue({
1139
+ processes: [],
1140
+ timedOut: false,
1141
+ });
1142
+ mockDiscoverPortsWithMeta.mockReturnValue({
1143
+ ports: [7777],
1144
+ timedOut: false,
1145
+ });
1146
+
1147
+ const listSignals: AbortSignal[] = [];
1148
+ const statusSignals: AbortSignal[] = [];
1149
+
1150
+ mockCreateOpencodeClient.mockImplementation(() => ({
1151
+ session: {
1152
+ list: vi.fn(({ signal }: { signal?: AbortSignal } = {}) => {
1153
+ if (signal) {
1154
+ listSignals.push(signal);
1155
+ }
1156
+ return createNeverResolvingPromise(signal);
1157
+ }),
1158
+ status: vi.fn(({ signal }: { signal?: AbortSignal } = {}) => {
1159
+ if (signal) {
1160
+ statusSignals.push(signal);
1161
+ }
1162
+ return createNeverResolvingPromise(signal);
1163
+ }),
1164
+ messages: mockSessionMessages,
1165
+ },
1166
+ }) as never);
1167
+
1168
+ const response = await freshGet();
1169
+ const data = await response.json();
1170
+
1171
+ expect(response.status).toBe(503);
1172
+ expect(data.error).toBe('Failed to fetch sessions from OpenCode ports');
1173
+ expect(data.failedPorts).toBeDefined();
1174
+ expect(Array.isArray(data.failedPorts)).toBe(true);
1175
+ expect(data.failedPorts.length).toBeGreaterThan(0);
1176
+ expect(String(data.failedPorts[0].reason)).toContain('timed out');
1177
+ expect(listSignals.length).toBeGreaterThan(0);
1178
+ expect(listSignals.every((signal) => signal.aborted)).toBe(true);
1179
+ if (statusSignals.length > 0) {
1180
+ expect(statusSignals.every((signal) => signal.aborted)).toBe(true);
1181
+ }
1182
+
1183
+ process.env.OPENCODE_SESSIONS_LIST_TIMEOUT_MS = originalListTimeoutEnv;
1184
+ process.env.OPENCODE_SESSIONS_STATUS_TIMEOUT_MS = originalStatusTimeoutEnv;
1185
+ });
1186
+
1187
+ it('keeps duplicate raw session ids from different hosts as distinct aggregate sessions', async () => {
1188
+ setupLocalSessionsMocks();
1189
+ mockSessionList.mockResolvedValue({
1190
+ data: [
1191
+ {
1192
+ id: 'shared-session',
1193
+ title: 'Local Shared Session',
1194
+ directory: '/repo/project-one',
1195
+ time: { created: 1_000, updated: Date.now() - 1_000 },
1196
+ },
1197
+ ],
1198
+ });
1199
+ mockSessionStatus.mockResolvedValue({
1200
+ data: {
1201
+ 'shared-session': { type: 'busy' },
1202
+ },
1203
+ });
1204
+
1205
+ mockListNodeRecords.mockResolvedValue([
1206
+ {
1207
+ nodeId: 'remote-shared',
1208
+ nodeLabel: 'Remote Shared',
1209
+ baseUrl: 'https://remote-shared.test',
1210
+ enabled: true,
1211
+ token: 'shared-token',
1212
+ createdAt: new Date().toISOString(),
1213
+ updatedAt: new Date().toISOString(),
1214
+ },
1215
+ ]);
1216
+
1217
+ const mockFetch = vi.fn(async (input: RequestInfo | URL) => {
1218
+ const url = typeof input === 'string' ? input : input.toString();
1219
+ if (url === 'https://remote-shared.test/api/node/sessions') {
1220
+ return new Response(
1221
+ JSON.stringify({
1222
+ ok: true,
1223
+ role: 'node',
1224
+ protocolVersion: NODE_PROTOCOL_VERSION,
1225
+ source: { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
1226
+ upstream: { kind: 'opencode', reachable: true },
1227
+ sessions: [
1228
+ {
1229
+ id: 'local:shared-session',
1230
+ rawSessionId: 'shared-session',
1231
+ sourceSessionKey: 'local:shared-session',
1232
+ title: 'Remote Shared Session',
1233
+ directory: '/remote/project-shared',
1234
+ projectName: 'project-shared',
1235
+ branch: null,
1236
+ realTimeStatus: 'idle',
1237
+ waitingForUser: false,
1238
+ time: { created: 2_000, updated: Date.now() - 800 },
1239
+ children: [],
1240
+ },
1241
+ ],
1242
+ processHints: [],
1243
+ hosts: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
1244
+ hostStatuses: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
1245
+ }),
1246
+ { status: 200, headers: { 'content-type': 'application/json' } }
1247
+ );
1248
+ }
1249
+ throw new Error(`Unexpected node sessions URL: ${url}`);
1250
+ });
1251
+ vi.stubGlobal('fetch', mockFetch);
1252
+
1253
+ mockCreateOpencodeClient.mockImplementation(({ baseUrl }: { baseUrl: string }) => {
1254
+ if (baseUrl === 'http://localhost:7777') {
1255
+ return {
1256
+ session: {
1257
+ list: mockSessionList,
1258
+ status: mockSessionStatus,
1259
+ messages: mockSessionMessages,
1260
+ },
1261
+ } as never;
1262
+ }
1263
+
1264
+ throw new Error(`Unexpected baseUrl: ${baseUrl}`);
1265
+ });
1266
+
1267
+ const response = await POST(
1268
+ new Request('http://localhost/api/sessions', {
1269
+ method: 'POST',
1270
+ headers: { 'content-type': 'application/json' },
1271
+ body: JSON.stringify({
1272
+ sources: [
1273
+ { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
1274
+ {
1275
+ hostId: 'remote-shared',
1276
+ hostLabel: 'Remote Shared',
1277
+ hostKind: 'remote',
1278
+ baseUrl: 'https://remote-shared.test',
1279
+ enabled: true,
1280
+ },
1281
+ ],
1282
+ }),
1283
+ })
1284
+ );
1285
+ const data = await response.json();
1286
+
1287
+ expect(response.status).toBe(200);
1288
+ expect(data.sessions).toHaveLength(2);
1289
+ expect(data.sessions.map((session: any) => session.id).sort()).toEqual([
1290
+ 'local:shared-session',
1291
+ 'remote-shared:shared-session',
1292
+ ]);
1293
+ expect(data.sessions.map((session: any) => session.rawSessionId)).toEqual([
1294
+ 'shared-session',
1295
+ 'shared-session',
1296
+ ]);
1297
+ });
1298
+ });