vibepulse 0.2.2 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -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 +8 -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/sessions/[id]/open-editor/route.js.nft.json +1 -1
  32. package/.next/server/app/api/node/sessions/route.js +5 -3
  33. package/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  34. package/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
  35. package/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
  36. package/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
  37. package/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
  38. package/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
  39. package/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
  40. package/.next/server/app/api/profiles/route.js.nft.json +1 -1
  41. package/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
  42. package/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  43. package/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  44. package/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  45. package/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
  46. package/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
  48. package/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
  49. package/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
  50. package/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
  51. package/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
  52. package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
  53. package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
  54. package/.next/server/app/api/sessions/route.js +4 -2
  55. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  56. package/.next/server/app/index.html +1 -1
  57. package/.next/server/app/index.rsc +3 -3
  58. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  59. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  60. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  61. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  62. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/page_client-reference-manifest.js +1 -1
  64. package/.next/server/app-paths-manifest.json +1 -0
  65. package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
  66. package/.next/server/chunks/[root-of-the-server]__31d19c5c._.js.map +1 -0
  67. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  68. package/.next/server/chunks/[root-of-the-server]__56f5f249._.js.map +1 -1
  69. package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
  70. package/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js.map +1 -0
  71. package/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
  72. package/.next/server/chunks/[root-of-the-server]__98073dd6._.js.map +1 -0
  73. package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
  74. package/.next/server/chunks/[root-of-the-server]__b7b717eb._.js.map +1 -0
  75. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
  76. package/.next/server/chunks/[root-of-the-server]__d8e61048._.js.map +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
  78. package/.next/server/chunks/[root-of-the-server]__f441109e._.js.map +1 -0
  79. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
  80. package/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js.map +1 -0
  81. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
  82. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js.map +1 -0
  83. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
  84. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js.map +1 -0
  85. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
  86. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js.map +1 -1
  87. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  88. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js.map +1 -1
  89. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
  90. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js.map +1 -0
  91. package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
  92. package/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js.map +1 -0
  93. package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
  94. package/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js.map → [root-of-the-server]__c91a8380._.js.map} +1 -1
  95. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  96. package/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js.map +1 -1
  97. package/.next/server/pages/404.html +1 -1
  98. package/.next/server/pages/500.html +2 -2
  99. package/.next/server/server-reference-manifest.js +1 -1
  100. package/.next/server/server-reference-manifest.json +1 -1
  101. package/.next/standalone/.next/BUILD_ID +1 -1
  102. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  103. package/.next/standalone/.next/build-manifest.json +2 -2
  104. package/.next/standalone/.next/prerender-manifest.json +3 -3
  105. package/.next/standalone/.next/routes-manifest.json +8 -0
  106. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  107. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  108. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  109. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  110. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  111. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  112. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  113. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  114. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  115. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  116. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  117. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  118. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  119. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  120. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  121. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  122. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  123. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  125. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/api/node/sessions/[id]/open-editor/route.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/api/node/sessions/route.js +5 -3
  128. package/.next/standalone/.next/server/app/api/node/sessions/route.js.nft.json +1 -1
  129. package/.next/standalone/.next/server/app/api/opencode-config/route.js.nft.json +1 -1
  130. package/.next/standalone/.next/server/app/api/opencode-config/status/route.js.nft.json +1 -1
  131. package/.next/standalone/.next/server/app/api/profiles/[id]/apply/route.js.nft.json +1 -1
  132. package/.next/standalone/.next/server/app/api/profiles/[id]/export/route.js.nft.json +1 -1
  133. package/.next/standalone/.next/server/app/api/profiles/[id]/route.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/api/profiles/import/route.js.nft.json +1 -1
  135. package/.next/standalone/.next/server/app/api/profiles/route.js.nft.json +1 -1
  136. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js +3 -2
  137. package/.next/standalone/.next/server/app/api/sessions/[id]/archive/route.js.nft.json +1 -1
  138. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js +3 -2
  139. package/.next/standalone/.next/server/app/api/sessions/[id]/delete/route.js.nft.json +1 -1
  140. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js +1 -1
  141. package/.next/standalone/.next/server/app/api/sessions/[id]/open-editor/route.js.nft.json +1 -1
  142. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
  143. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
  144. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
  145. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js +8 -0
  146. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
  147. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
  148. package/.next/standalone/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
  149. package/.next/standalone/.next/server/app/api/sessions/route.js +4 -2
  150. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  151. package/.next/standalone/.next/server/app/index.html +1 -1
  152. package/.next/standalone/.next/server/app/index.rsc +3 -3
  153. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  154. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  155. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  156. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  157. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  158. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  159. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  160. package/.next/standalone/.next/server/chunks/[root-of-the-server]__31d19c5c._.js +3 -0
  161. package/.next/standalone/.next/server/chunks/[root-of-the-server]__56f5f249._.js +1 -1
  162. package/.next/standalone/.next/server/chunks/[root-of-the-server]__5e0a0e38._.js +3 -0
  163. package/.next/standalone/.next/server/chunks/[root-of-the-server]__98073dd6._.js +3 -0
  164. package/.next/standalone/.next/server/chunks/[root-of-the-server]__b7b717eb._.js +3 -0
  165. package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8e61048._.js +1 -1
  166. package/.next/standalone/.next/server/chunks/[root-of-the-server]__f441109e._.js +3 -0
  167. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_sessions_[id]_restore_route_actions_af7d6b6c.js +3 -0
  168. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_2edc9589.js +3 -0
  169. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_7f178d4a.js +3 -0
  170. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_aca45402.js +1 -1
  171. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_b054aff3.js +1 -1
  172. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d0c0f338.js +3 -0
  173. package/.next/standalone/.next/server/chunks/src_lib_session-providers_claudeCode_ts_0f9590ed._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__631e12d0._.js → [root-of-the-server]__c91a8380._.js} +2 -2
  175. package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_a7111f3e._.js +3 -3
  176. package/.next/standalone/.next/server/pages/404.html +1 -1
  177. package/.next/standalone/.next/server/pages/500.html +2 -2
  178. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  179. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  180. package/.next/standalone/.next/static/chunks/b3bc362202331708.css +3 -0
  181. package/.next/standalone/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
  182. package/.next/standalone/README.md +29 -5
  183. package/.next/standalone/docs/session-status-detection.md +36 -0
  184. package/.next/standalone/docs/superpowers/specs/2026-04-09-claude-capability-alignment-design.md +39 -0
  185. package/.next/standalone/package-lock.json +2 -2
  186. package/.next/standalone/package.json +1 -1
  187. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.test.ts +60 -1
  188. package/.next/standalone/src/app/api/node/sessions/[id]/archive/route.ts +77 -22
  189. package/.next/standalone/src/app/api/node/sessions/route.test.ts +282 -0
  190. package/.next/standalone/src/app/api/node/sessions/route.ts +141 -17
  191. package/.next/standalone/src/app/api/opencode-events/route.test.ts +3 -1
  192. package/.next/standalone/src/app/api/sessions/[id]/archive/route.test.ts +101 -0
  193. package/.next/standalone/src/app/api/sessions/[id]/archive/route.ts +47 -12
  194. package/.next/standalone/src/app/api/sessions/[id]/delete/route.test.ts +92 -0
  195. package/.next/standalone/src/app/api/sessions/[id]/delete/route.ts +45 -10
  196. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.test.ts +74 -0
  197. package/.next/standalone/src/app/api/sessions/[id]/open-editor/route.ts +22 -2
  198. package/.next/standalone/src/app/api/sessions/[id]/restore/route.test.ts +186 -0
  199. package/.next/standalone/src/app/api/sessions/[id]/restore/route.ts +184 -0
  200. package/.next/standalone/src/app/api/sessions/route.test.ts +1889 -107
  201. package/.next/standalone/src/app/api/sessions/route.ts +365 -981
  202. package/.next/standalone/src/components/KanbanBoard.test.tsx +307 -1
  203. package/.next/standalone/src/components/KanbanBoard.tsx +105 -18
  204. package/.next/standalone/src/components/ProjectCard.test.tsx +416 -2
  205. package/.next/standalone/src/components/ProjectCard.tsx +238 -86
  206. package/.next/standalone/src/components/SessionCard.test.tsx +253 -2
  207. package/.next/standalone/src/components/SessionCard.tsx +182 -76
  208. package/.next/standalone/src/hooks/useOpencodeSync.test.ts +321 -1
  209. package/.next/standalone/src/hooks/useOpencodeSync.ts +16 -12
  210. package/.next/standalone/src/lib/claudeSessionOverrides.test.ts +75 -0
  211. package/.next/standalone/src/lib/claudeSessionOverrides.ts +169 -0
  212. package/.next/standalone/src/lib/session-providers/claudeCode.test.ts +2288 -0
  213. package/.next/standalone/src/lib/session-providers/claudeCode.ts +1083 -0
  214. package/.next/standalone/src/lib/session-providers/localAggregator.test.ts +322 -0
  215. package/.next/standalone/src/lib/session-providers/localAggregator.ts +302 -0
  216. package/.next/standalone/src/lib/session-providers/opencodeProvider.ts +723 -0
  217. package/.next/standalone/src/lib/session-providers/providerIds.test.ts +337 -0
  218. package/.next/standalone/src/lib/session-providers/providerIds.ts +176 -0
  219. package/.next/standalone/src/lib/session-providers/types.ts +131 -0
  220. package/.next/standalone/src/lib/transform.test.ts +253 -0
  221. package/.next/standalone/src/lib/transform.ts +96 -37
  222. package/.next/standalone/src/types/index.ts +23 -17
  223. package/.next/static/chunks/b3bc362202331708.css +3 -0
  224. package/.next/static/chunks/{65d5354ba0add961.js → c1294e057d8d4681.js} +3 -3
  225. package/.next/trace +1 -1
  226. package/.next/trace-build +1 -1
  227. package/.next/types/routes.d.ts +2 -1
  228. package/.next/types/validator.ts +9 -0
  229. package/README.md +29 -5
  230. package/package.json +1 -1
  231. package/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
  232. package/.next/server/chunks/[root-of-the-server]__2f981540._.js.map +0 -1
  233. package/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
  234. package/.next/server/chunks/[root-of-the-server]__3745b314._.js.map +0 -1
  235. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
  236. package/.next/server/chunks/[root-of-the-server]__6c428a24._.js.map +0 -1
  237. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
  238. package/.next/server/chunks/[root-of-the-server]__73a00b88._.js.map +0 -1
  239. package/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
  240. package/.next/server/chunks/[root-of-the-server]__db285678._.js.map +0 -1
  241. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2f981540._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/[root-of-the-server]__3745b314._.js +0 -3
  243. package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c428a24._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/[root-of-the-server]__73a00b88._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/[root-of-the-server]__db285678._.js +0 -3
  246. package/.next/standalone/.next/static/chunks/f42202943f6742e5.css +0 -3
  247. package/.next/static/chunks/f42202943f6742e5.css +0 -3
  248. /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
  249. /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
  250. /package/.next/standalone/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
  251. /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_buildManifest.js +0 -0
  252. /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_clientMiddlewareManifest.json +0 -0
  253. /package/.next/static/{5kq9DtuBFVxu4jsgmL5Q- → bsWNvgDS7Zp38Yt9q0DUg}/_ssgManifest.js +0 -0
@@ -13,6 +13,12 @@ vi.mock('@/lib/opencodeConfig', () => ({
13
13
  readConfig: vi.fn(),
14
14
  }));
15
15
 
16
+ vi.mock('@/lib/session-providers/claudeCode', () => ({
17
+ claudeCodeLocalSessionProvider: {
18
+ getSessionsResult: vi.fn(),
19
+ },
20
+ }));
21
+
16
22
  vi.mock('child_process', async () => {
17
23
  const execSync = vi.fn();
18
24
  return {
@@ -39,6 +45,7 @@ import {
39
45
  discoverOpencodeProcessCwdsWithoutPortWithMeta,
40
46
  } from '@/lib/opencodeDiscovery';
41
47
  import { readConfig } from '@/lib/opencodeConfig';
48
+ import { claudeCodeLocalSessionProvider } from '@/lib/session-providers/claudeCode';
42
49
  import { createNodeRequestHeaders } from '@/lib/nodeProtocol';
43
50
 
44
51
  import { GET } from './route';
@@ -50,6 +57,7 @@ const mockCreateOpencodeClient: any = createOpencodeClient;
50
57
  const mockDiscoverPortsWithMeta: any = discoverOpencodePortsWithMeta;
51
58
  const mockDiscoverProcessCwdsWithoutPortWithMeta: any = discoverOpencodeProcessCwdsWithoutPortWithMeta;
52
59
  const mockReadConfig: any = readConfig;
60
+ const mockClaudeLocalProviderGetSessionsResult: any = claudeCodeLocalSessionProvider.getSessionsResult;
53
61
  const mockExecSync: any = execSync;
54
62
 
55
63
  function resetDefaultClientMock(): void {
@@ -82,6 +90,13 @@ function createNeverResolvingPromise<T>(signal?: AbortSignal): Promise<T> {
82
90
 
83
91
  function setupLocalSessionsMocks(): void {
84
92
  resetDefaultClientMock();
93
+ mockClaudeLocalProviderGetSessionsResult.mockResolvedValue({
94
+ payload: {
95
+ sessions: [],
96
+ processHints: [],
97
+ },
98
+ sourceMeta: { online: false },
99
+ });
85
100
 
86
101
  mockReadConfig.mockResolvedValue({
87
102
  vibepulse: {
@@ -225,9 +240,276 @@ describe('/api/node/sessions', () => {
225
240
  waitingForUser: true,
226
241
  });
227
242
  expect(JSON.stringify(data).includes('baseUrl')).toBe(false);
243
+ expect(data.sessions.every((session: any) => session.hostId === 'local')).toBe(true);
244
+ expect(data.sessions.every((session: any) => !session.baseUrl)).toBe(true);
228
245
  expect(mockCreateOpencodeClient.mock.calls).toEqual([[{ baseUrl: 'http://localhost:7777' }]]);
229
246
  });
230
247
 
248
+ it('carries nested Claude child topology in node polling payloads with provider-aware local ids', async () => {
249
+ mockClaudeLocalProviderGetSessionsResult.mockResolvedValue({
250
+ payload: {
251
+ sessions: [
252
+ {
253
+ id: 'claude~550e8400-e29b-41d4-a716-446655440000',
254
+ slug: '550e8400-e29b-41d4-a716-446655440000',
255
+ title: 'Claude Parent',
256
+ directory: '/repo/project-one',
257
+ projectName: 'project-one',
258
+ branch: 'main',
259
+ provider: 'claude-code',
260
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
261
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
262
+ topology: { childSessions: 'authoritative' },
263
+ readOnly: true,
264
+ realTimeStatus: 'busy',
265
+ waitingForUser: false,
266
+ children: [
267
+ {
268
+ id: '660e8400-e29b-41d4-a716-446655440000',
269
+ parentID: '550e8400-e29b-41d4-a716-446655440000',
270
+ rawSessionId: '660e8400-e29b-41d4-a716-446655440000',
271
+ providerRawId: '660e8400-e29b-41d4-a716-446655440000',
272
+ title: 'Claude Child',
273
+ directory: '/repo/project-one',
274
+ realTimeStatus: 'busy',
275
+ waitingForUser: false,
276
+ readOnly: true,
277
+ topology: { childSessions: 'authoritative' },
278
+ time: { created: 2_100, updated: Date.now() - 900 },
279
+ },
280
+ ],
281
+ time: { created: 2_000, updated: Date.now() - 1_000 },
282
+ },
283
+ ],
284
+ processHints: [],
285
+ },
286
+ sourceMeta: { online: true },
287
+ });
288
+
289
+ const response = await GET(
290
+ new Request('http://localhost/api/node/sessions', {
291
+ headers: createNodeRequestHeaders('shared-secret'),
292
+ })
293
+ );
294
+ const data = await response.json();
295
+
296
+ expect(response.status).toBe(200);
297
+ expect(data.sessions).toEqual(
298
+ expect.arrayContaining([
299
+ expect.objectContaining({ id: 'local:parent-1' }),
300
+ expect.objectContaining({
301
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
302
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
303
+ sourceSessionKey: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
304
+ provider: 'claude-code',
305
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
306
+ readOnly: true,
307
+ topology: { childSessions: 'authoritative' },
308
+ children: [
309
+ expect.objectContaining({
310
+ id: 'local:claude~660e8400-e29b-41d4-a716-446655440000',
311
+ rawSessionId: '660e8400-e29b-41d4-a716-446655440000',
312
+ sourceSessionKey: 'local:claude~660e8400-e29b-41d4-a716-446655440000',
313
+ parentID: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
314
+ provider: 'claude-code',
315
+ providerRawId: '660e8400-e29b-41d4-a716-446655440000',
316
+ hostId: 'local',
317
+ hostLabel: 'Local',
318
+ hostKind: 'local',
319
+ readOnly: true,
320
+ topology: { childSessions: 'authoritative' },
321
+ }),
322
+ ],
323
+ }),
324
+ ])
325
+ );
326
+ });
327
+
328
+ it('keeps flat Claude polling payloads backward-compatible when provider metadata is sparse', async () => {
329
+ mockClaudeLocalProviderGetSessionsResult.mockResolvedValue({
330
+ payload: {
331
+ sessions: [
332
+ {
333
+ id: 'claude~550e8400-e29b-41d4-a716-446655440000',
334
+ slug: '550e8400-e29b-41d4-a716-446655440000',
335
+ title: 'Claude Flat Session',
336
+ directory: '/repo/project-one',
337
+ projectName: 'project-one',
338
+ branch: 'main',
339
+ realTimeStatus: 'idle',
340
+ waitingForUser: false,
341
+ readOnly: true,
342
+ children: [],
343
+ time: { created: 3_000, updated: Date.now() - 1_500 },
344
+ },
345
+ ],
346
+ processHints: [],
347
+ },
348
+ sourceMeta: { online: true },
349
+ });
350
+
351
+ const response = await GET(
352
+ new Request('http://localhost/api/node/sessions', {
353
+ headers: createNodeRequestHeaders('shared-secret'),
354
+ })
355
+ );
356
+ const data = await response.json();
357
+
358
+ expect(response.status).toBe(200);
359
+ expect(data.sessions).toEqual(
360
+ expect.arrayContaining([
361
+ expect.objectContaining({
362
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
363
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
364
+ sourceSessionKey: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
365
+ provider: 'claude-code',
366
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
367
+ readOnly: true,
368
+ children: [],
369
+ }),
370
+ ])
371
+ );
372
+ });
373
+
374
+ it('returns Claude polling sessions when OpenCode ports are absent but Claude provider is available', async () => {
375
+ mockDiscoverProcessCwdsWithoutPortWithMeta.mockReturnValue({
376
+ processes: [],
377
+ timedOut: false,
378
+ });
379
+ mockDiscoverPortsWithMeta.mockReturnValue({
380
+ ports: [],
381
+ timedOut: false,
382
+ });
383
+ mockClaudeLocalProviderGetSessionsResult.mockResolvedValue({
384
+ payload: {
385
+ sessions: [
386
+ {
387
+ id: 'claude~550e8400-e29b-41d4-a716-446655440000',
388
+ slug: '550e8400-e29b-41d4-a716-446655440000',
389
+ title: 'Claude Only Session',
390
+ directory: '/repo/claude-only-project',
391
+ projectName: 'claude-only-project',
392
+ branch: 'main',
393
+ provider: 'claude-code',
394
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
395
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
396
+ readOnly: true,
397
+ topology: { childSessions: 'flat' },
398
+ realTimeStatus: 'busy',
399
+ waitingForUser: false,
400
+ children: [],
401
+ time: { created: 4_000, updated: Date.now() - 1_000 },
402
+ },
403
+ ],
404
+ processHints: [],
405
+ },
406
+ sourceMeta: { online: true },
407
+ });
408
+
409
+ const response = await GET(
410
+ new Request('http://localhost/api/node/sessions', {
411
+ headers: createNodeRequestHeaders('shared-secret'),
412
+ })
413
+ );
414
+ const data = await response.json();
415
+
416
+ expect(response.status).toBe(200);
417
+ expect(data).toMatchObject({
418
+ ok: true,
419
+ role: 'node',
420
+ protocolVersion: '1',
421
+ source: { hostId: 'local', hostLabel: 'Local', hostKind: 'local' },
422
+ upstream: { kind: 'opencode', reachable: true },
423
+ processHints: [],
424
+ hosts: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
425
+ hostStatuses: [{ hostId: 'local', hostLabel: 'Local', hostKind: 'local', online: true }],
426
+ });
427
+ expect(data.sessions).toEqual([
428
+ expect.objectContaining({
429
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
430
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
431
+ sourceSessionKey: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
432
+ provider: 'claude-code',
433
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
434
+ hostId: 'local',
435
+ hostLabel: 'Local',
436
+ hostKind: 'local',
437
+ readOnly: true,
438
+ topology: { childSessions: 'flat' },
439
+ children: [],
440
+ }),
441
+ ]);
442
+ expect(mockCreateOpencodeClient).not.toHaveBeenCalled();
443
+ });
444
+
445
+ it('returns degraded Claude fallback sessions when all discovered OpenCode ports fail', async () => {
446
+ setupLocalSessionsMocks();
447
+ mockDiscoverPortsWithMeta.mockReturnValue({
448
+ ports: [7777, 7778],
449
+ timedOut: false,
450
+ });
451
+ mockSessionList.mockRejectedValue(new Error('ECONNREFUSED'));
452
+ mockClaudeLocalProviderGetSessionsResult.mockResolvedValue({
453
+ payload: {
454
+ sessions: [
455
+ {
456
+ id: 'claude~550e8400-e29b-41d4-a716-446655440000',
457
+ slug: '550e8400-e29b-41d4-a716-446655440000',
458
+ title: 'Claude Fallback Session',
459
+ directory: '/repo/claude-fallback',
460
+ projectName: 'claude-fallback',
461
+ branch: 'main',
462
+ provider: 'claude-code',
463
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
464
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
465
+ readOnly: true,
466
+ topology: { childSessions: 'flat' },
467
+ realTimeStatus: 'busy',
468
+ waitingForUser: false,
469
+ children: [],
470
+ time: { created: 5_000, updated: Date.now() - 1_000 },
471
+ },
472
+ ],
473
+ processHints: [],
474
+ },
475
+ sourceMeta: { online: true },
476
+ });
477
+
478
+ const response = await GET(
479
+ new Request('http://localhost/api/node/sessions', {
480
+ headers: createNodeRequestHeaders('shared-secret'),
481
+ })
482
+ );
483
+ const data = await response.json();
484
+
485
+ expect(response.status).toBe(200);
486
+ expect(data.degraded).toBe(true);
487
+ expect(data.failedPorts).toEqual([
488
+ expect.objectContaining({ port: 7777 }),
489
+ expect.objectContaining({ port: 7778 }),
490
+ ]);
491
+ expect(data.sessions).toEqual(
492
+ expect.arrayContaining([
493
+ expect.objectContaining({
494
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
495
+ provider: 'claude-code',
496
+ sourceSessionKey: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
497
+ }),
498
+ ])
499
+ );
500
+ expect(data.hostStatuses).toEqual([
501
+ expect.objectContaining({
502
+ hostId: 'local',
503
+ online: true,
504
+ degraded: true,
505
+ }),
506
+ ]);
507
+ expect(mockCreateOpencodeClient.mock.calls).toEqual([
508
+ [{ baseUrl: 'http://localhost:7777' }],
509
+ [{ baseUrl: 'http://localhost:7778' }],
510
+ ]);
511
+ });
512
+
231
513
  it('rejects unauthenticated requests before running discovery', async () => {
232
514
  const response = await GET(
233
515
  new Request('http://localhost/api/node/sessions', {
@@ -6,7 +6,12 @@ import {
6
6
  discoverOpencodeProcessCwdsWithoutPortWithMeta,
7
7
  } from '@/lib/opencodeDiscovery';
8
8
  import { readConfig } from '@/lib/opencodeConfig';
9
- import { composeSourceKey } from '@/lib/hostIdentity';
9
+ import { claudeCodeLocalSessionProvider } from '@/lib/session-providers/claudeCode';
10
+ import {
11
+ composeProviderSourceKey,
12
+ detectProviderFromRawId,
13
+ extractProviderRawId,
14
+ } from '@/lib/session-providers/providerIds';
10
15
  import {
11
16
  NODE_PROTOCOL_VERSION,
12
17
  createNodeFailureResponse,
@@ -73,9 +78,20 @@ type HostAwareFields = {
73
78
  hostId?: typeof LOCAL_SOURCE.hostId;
74
79
  hostLabel?: typeof LOCAL_SOURCE.hostLabel;
75
80
  hostKind?: typeof LOCAL_SOURCE.hostKind;
81
+ provider?: 'opencode' | 'claude-code';
82
+ providerRawId?: string;
76
83
  rawSessionId?: string;
77
84
  sourceSessionKey?: string;
78
85
  readOnly?: boolean;
86
+ capabilities?: {
87
+ openProject: boolean;
88
+ openEditor: boolean;
89
+ archive: boolean;
90
+ delete: boolean;
91
+ };
92
+ topology?: {
93
+ childSessions: 'flat' | 'authoritative';
94
+ };
79
95
  };
80
96
 
81
97
  type ChildEntry = HostAwareFields & {
@@ -557,38 +573,112 @@ function sortChildEntries(children: ChildEntry[]): void {
557
573
  });
558
574
  }
559
575
 
560
- function addLocalHostMetadataToChildEntry(child: ChildEntry): ChildEntry {
561
- const rawSessionId = child.rawSessionId ?? child.id;
562
- const sourceSessionKey = composeSourceKey(LOCAL_SOURCE.hostId, rawSessionId);
576
+ function readSupplementalProviderPayload(payload: unknown): Pick<LocalSessionsSuccessPayload, 'sessions' | 'processHints'> {
577
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
578
+ return { sessions: [], processHints: [] };
579
+ }
580
+
581
+ const record = payload as Record<string, unknown>;
582
+
583
+ return {
584
+ sessions: Array.isArray(record['sessions']) ? (record['sessions'] as EnrichedSession[]) : [],
585
+ processHints: Array.isArray(record['processHints']) ? (record['processHints'] as ProcessHint[]) : [],
586
+ };
587
+ }
588
+
589
+ async function getSupplementalClaudePayload(stickyBusyDelayMs: number): Promise<
590
+ Pick<LocalSessionsSuccessPayload, 'sessions' | 'processHints'>
591
+ > {
592
+ try {
593
+ const result = await claudeCodeLocalSessionProvider.getSessionsResult({ stickyBusyDelayMs });
594
+ return readSupplementalProviderPayload(result.payload);
595
+ } catch {
596
+ return { sessions: [], processHints: [] };
597
+ }
598
+ }
599
+
600
+ function composeLocalProviderSourceKey(
601
+ rawSessionId: string,
602
+ fields: Pick<HostAwareFields, 'provider' | 'readOnly' | 'capabilities' | 'topology'>
603
+ ) {
604
+ return composeProviderSourceKey(LOCAL_SOURCE.hostId, rawSessionId, {
605
+ ...(fields.provider ? { provider: fields.provider } : {}),
606
+ ...(fields.readOnly !== undefined ? { readOnly: fields.readOnly } : {}),
607
+ ...(fields.capabilities ? { capabilities: fields.capabilities } : {}),
608
+ ...(fields.topology ? { topology: fields.topology } : {}),
609
+ });
610
+ }
611
+
612
+ function addLocalHostMetadataToChildEntry(
613
+ child: ChildEntry,
614
+ parentSourceSessionKey?: string,
615
+ parentProvider?: HostAwareFields['provider']
616
+ ): ChildEntry {
617
+ const rawSessionId = child.rawSessionId ?? extractProviderRawId(child.id);
618
+ const inferredProvider = child.provider ?? detectProviderFromRawId(child.id);
619
+ const provider = inferredProvider === 'claude-code' ? inferredProvider : (parentProvider ?? inferredProvider);
620
+ const sourceKey = composeLocalProviderSourceKey(rawSessionId, {
621
+ provider,
622
+ readOnly: child.readOnly,
623
+ capabilities: child.capabilities,
624
+ topology: child.topology,
625
+ });
626
+ const rawParentId = child.parentID ? extractProviderRawId(child.parentID) : undefined;
627
+ const sourceParentKey = parentSourceSessionKey
628
+ ?? (rawParentId
629
+ ? composeLocalProviderSourceKey(rawParentId, {
630
+ provider,
631
+ }).sourceKey
632
+ : child.parentID);
563
633
 
564
634
  return {
565
635
  ...child,
566
- id: sourceSessionKey,
567
- parentID: child.parentID ? composeSourceKey(LOCAL_SOURCE.hostId, child.parentID) : child.parentID,
636
+ id: sourceKey.sourceKey,
637
+ parentID: sourceParentKey,
568
638
  hostId: LOCAL_SOURCE.hostId,
569
639
  hostLabel: LOCAL_SOURCE.hostLabel,
570
640
  hostKind: LOCAL_SOURCE.hostKind,
571
641
  rawSessionId,
572
- sourceSessionKey,
573
- readOnly: false,
642
+ sourceSessionKey: sourceKey.sourceKey,
643
+ provider,
644
+ providerRawId: child.providerRawId ?? sourceKey.providerRawId,
645
+ readOnly: child.readOnly ?? sourceKey.readOnly,
646
+ capabilities: child.capabilities ?? sourceKey.capabilities,
647
+ topology: child.topology ?? sourceKey.topology,
574
648
  };
575
649
  }
576
650
 
577
651
  function addLocalHostMetadataToSession(session: EnrichedSession): EnrichedSession {
578
- const rawSessionId = session.rawSessionId ?? session.id;
579
- const sourceSessionKey = composeSourceKey(LOCAL_SOURCE.hostId, rawSessionId);
652
+ const rawSessionId = session.rawSessionId ?? extractProviderRawId(session.id);
653
+ const provider = session.provider ?? detectProviderFromRawId(session.id);
654
+ const sourceKey = composeLocalProviderSourceKey(rawSessionId, {
655
+ provider,
656
+ readOnly: session.readOnly,
657
+ capabilities: session.capabilities,
658
+ topology: session.topology,
659
+ });
660
+ const rawParentId = session.parentID ? extractProviderRawId(session.parentID) : undefined;
661
+ const sourceParentKey = rawParentId
662
+ ? composeLocalProviderSourceKey(rawParentId, {
663
+ provider,
664
+ }).sourceKey
665
+ : session.parentID;
580
666
 
581
667
  return {
582
668
  ...session,
583
- id: sourceSessionKey,
584
- parentID: session.parentID ? composeSourceKey(LOCAL_SOURCE.hostId, session.parentID) : session.parentID,
669
+ id: sourceKey.sourceKey,
670
+ parentID: sourceParentKey,
585
671
  hostId: LOCAL_SOURCE.hostId,
586
672
  hostLabel: LOCAL_SOURCE.hostLabel,
587
673
  hostKind: LOCAL_SOURCE.hostKind,
588
674
  rawSessionId,
589
- sourceSessionKey,
590
- readOnly: false,
591
- children: session.children.map((child) => addLocalHostMetadataToChildEntry(child)),
675
+ sourceSessionKey: sourceKey.sourceKey,
676
+ provider,
677
+ providerRawId: session.providerRawId ?? sourceKey.providerRawId,
678
+ readOnly: session.readOnly ?? sourceKey.readOnly,
679
+ capabilities: session.capabilities ?? sourceKey.capabilities,
680
+ topology: session.topology ?? sourceKey.topology,
681
+ children: session.children.map((child) => addLocalHostMetadataToChildEntry(child, sourceKey.sourceKey, provider)),
592
682
  };
593
683
  }
594
684
 
@@ -686,6 +776,20 @@ async function getLocalSessionsResult(stickyBusyDelayMs: number): Promise<LocalS
686
776
  return createLocalFailureResult('upstream_timeout', processHints);
687
777
  }
688
778
 
779
+ const supplementalClaudePayload = await getSupplementalClaudePayload(stickyBusyDelayMs);
780
+ if (supplementalClaudePayload.sessions.length > 0 || supplementalClaudePayload.processHints.length > 0) {
781
+ return {
782
+ ok: true,
783
+ payload: {
784
+ sessions: supplementalClaudePayload.sessions,
785
+ processHints: [...processHints, ...supplementalClaudePayload.processHints],
786
+ },
787
+ meta: {
788
+ online: true,
789
+ },
790
+ };
791
+ }
792
+
689
793
  return createLocalFailureResult('upstream_unreachable', processHints);
690
794
  }
691
795
 
@@ -771,6 +875,24 @@ async function getLocalSessionsResult(stickyBusyDelayMs: number): Promise<LocalS
771
875
  }
772
876
 
773
877
  if (results.length > 0 && failedPorts.length === results.length) {
878
+ const supplementalClaudePayload = await getSupplementalClaudePayload(stickyBusyDelayMs);
879
+ if (supplementalClaudePayload.sessions.length > 0 || supplementalClaudePayload.processHints.length > 0) {
880
+ return {
881
+ ok: true,
882
+ payload: {
883
+ sessions: supplementalClaudePayload.sessions,
884
+ processHints: [...processHints, ...supplementalClaudePayload.processHints],
885
+ failedPorts,
886
+ degraded: true,
887
+ },
888
+ meta: {
889
+ online: true,
890
+ degraded: true,
891
+ reason: resolveFailureReasonFromMessages(failedPorts.map((entry) => entry.reason)),
892
+ },
893
+ };
894
+ }
895
+
774
896
  pruneStickyState(Date.now(), new Set<string>());
775
897
  return createLocalFailureResult(resolveFailureReasonFromMessages(failedPorts.map((entry) => entry.reason)), processHints, {
776
898
  failedPorts,
@@ -1069,11 +1191,13 @@ async function getLocalSessionsResult(stickyBusyDelayMs: number): Promise<LocalS
1069
1191
 
1070
1192
  const filteredProcessHints = processHints.filter((hint) => !knownDirectories.has(hint.directory));
1071
1193
 
1194
+ const supplementalClaudePayload = await getSupplementalClaudePayload(stickyBusyDelayMs);
1195
+
1072
1196
  return {
1073
1197
  ok: true,
1074
1198
  payload: {
1075
- sessions: enrichedSessions,
1076
- processHints: filteredProcessHints,
1199
+ sessions: [...enrichedSessions, ...supplementalClaudePayload.sessions],
1200
+ processHints: [...filteredProcessHints, ...supplementalClaudePayload.processHints],
1077
1201
  ...(failedPorts.length > 0 ? { failedPorts, degraded: true } : {}),
1078
1202
  },
1079
1203
  meta: {
@@ -493,7 +493,7 @@ describe('/api/opencode-events', () => {
493
493
  const reader = response.body?.getReader();
494
494
  expect(reader).toBeTruthy();
495
495
 
496
- const first = await readPayload(reader!);
496
+ const first = (await readPayload(reader!)) as any;
497
497
  expect(first).toEqual({
498
498
  type: 'session.status',
499
499
  properties: {
@@ -502,6 +502,8 @@ describe('/api/opencode-events', () => {
502
502
  },
503
503
  timestamp: 100,
504
504
  });
505
+ expect(first.source).toBeUndefined();
506
+ expect(first.event).toBeUndefined();
505
507
 
506
508
  await reader!.cancel();
507
509
  });
@@ -13,13 +13,19 @@ vi.mock('@/lib/sessionArchiveOverrides', () => ({
13
13
  markSessionStickyStatusBlocked: vi.fn(),
14
14
  }));
15
15
 
16
+ vi.mock('@/lib/claudeSessionOverrides', () => ({
17
+ markClaudeSessionArchived: vi.fn(),
18
+ }));
19
+
16
20
  import { discoverOpencodePortsWithMeta } from '@/lib/opencodeDiscovery';
17
21
  import { listNodeRecords } from '@/lib/nodeRegistry';
22
+ import { markClaudeSessionArchived } from '@/lib/claudeSessionOverrides';
18
23
 
19
24
  import { POST } from './route';
20
25
 
21
26
  const mockDiscoverPortsWithMeta: any = discoverOpencodePortsWithMeta;
22
27
  const mockListNodeRecords: any = listNodeRecords;
28
+ const mockMarkClaudeSessionArchived: any = markClaudeSessionArchived;
23
29
 
24
30
  describe('/api/sessions/[id]/archive', () => {
25
31
  beforeEach(() => {
@@ -40,6 +46,23 @@ describe('/api/sessions/[id]/archive', () => {
40
46
  expect(mockFetch).toHaveBeenCalledWith('http://localhost:7777/session/abc', expect.objectContaining({ method: 'PATCH' }));
41
47
  });
42
48
 
49
+ it('treats UUID-like local ids without claude namespace as opencode sessions', async () => {
50
+ const opencodeUuid = '550e8400-e29b-41d4-a716-446655440000';
51
+ const mockFetch = vi.fn(async () => new Response(JSON.stringify({ error: 'missing' }), { status: 404 }));
52
+ vi.stubGlobal('fetch', mockFetch);
53
+
54
+ const response = await POST(new Request(`http://localhost/api/sessions/local:${opencodeUuid}/archive`, { method: 'POST' }), {
55
+ params: Promise.resolve({ id: `local:${opencodeUuid}` }),
56
+ });
57
+ const data = await response.json();
58
+
59
+ expect(response.status).toBe(404);
60
+ expect(data).toEqual({ error: 'Session not found', reason: 'session_not_found' });
61
+ expect(mockMarkClaudeSessionArchived).not.toHaveBeenCalled();
62
+ expect(mockDiscoverPortsWithMeta).toHaveBeenCalled();
63
+ expect(mockFetch).toHaveBeenCalledWith(`http://localhost:7777/session/${opencodeUuid}`, expect.objectContaining({ method: 'PATCH' }));
64
+ });
65
+
43
66
  it('forwards remote archive ids to the matching node endpoint', async () => {
44
67
  mockListNodeRecords.mockResolvedValue([
45
68
  {
@@ -98,6 +121,84 @@ describe('/api/sessions/[id]/archive', () => {
98
121
  expect(data).toEqual({ error: 'Session not found', reason: 'session_not_found' });
99
122
  });
100
123
 
124
+ it('archives Claude sessions through local override storage before any OpenCode execution', async () => {
125
+ const mockFetch = vi.fn();
126
+ vi.stubGlobal('fetch', mockFetch);
127
+
128
+ const response = await POST(new Request('http://localhost/api/sessions/local:claude~550e8400-e29b-41d4-a716-446655440000/archive', { method: 'POST' }), {
129
+ params: Promise.resolve({ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000' }),
130
+ });
131
+ const data = await response.json();
132
+
133
+ expect(response.status).toBe(200);
134
+ expect(data).toEqual({ success: true });
135
+ expect(mockMarkClaudeSessionArchived).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
136
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
137
+ expect(mockFetch).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('archives scoped Claude sidechain sessions through local override storage', async () => {
141
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
142
+ const mockFetch = vi.fn();
143
+ vi.stubGlobal('fetch', mockFetch);
144
+
145
+ const response = await POST(new Request(`http://localhost/api/sessions/local:${scopedSessionId}/archive`, { method: 'POST' }), {
146
+ params: Promise.resolve({ id: `local:${scopedSessionId}` }),
147
+ });
148
+ const data = await response.json();
149
+
150
+ expect(response.status).toBe(200);
151
+ expect(data).toEqual({ success: true });
152
+ expect(mockMarkClaudeSessionArchived).toHaveBeenCalledWith(scopedSessionId);
153
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
154
+ expect(mockFetch).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('rejects remote Claude archive requests before local override or node execution', async () => {
158
+ const mockFetch = vi.fn();
159
+ vi.stubGlobal('fetch', mockFetch);
160
+
161
+ const response = await POST(new Request('http://localhost/api/sessions/node-1:claude~550e8400-e29b-41d4-a716-446655440000/archive', { method: 'POST' }), {
162
+ params: Promise.resolve({ id: 'node-1:claude~550e8400-e29b-41d4-a716-446655440000' }),
163
+ });
164
+ const data = await response.json();
165
+
166
+ expect(response.status).toBe(403);
167
+ expect(data).toEqual({
168
+ error: 'Session action not supported by provider',
169
+ reason: 'provider_capability_unsupported',
170
+ provider: 'claude-code',
171
+ capability: 'archive',
172
+ });
173
+ expect(mockMarkClaudeSessionArchived).not.toHaveBeenCalled();
174
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
175
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
176
+ expect(mockFetch).not.toHaveBeenCalled();
177
+ });
178
+
179
+ it('rejects remote scoped Claude sidechain archive requests before node execution', async () => {
180
+ const scopedSessionId = '550e8400-e29b-41d4-a716-446655440000__agent-a123';
181
+ const mockFetch = vi.fn();
182
+ vi.stubGlobal('fetch', mockFetch);
183
+
184
+ const response = await POST(new Request(`http://localhost/api/sessions/node-1:${scopedSessionId}/archive`, { method: 'POST' }), {
185
+ params: Promise.resolve({ id: `node-1:${scopedSessionId}` }),
186
+ });
187
+ const data = await response.json();
188
+
189
+ expect(response.status).toBe(403);
190
+ expect(data).toEqual({
191
+ error: 'Session action not supported by provider',
192
+ reason: 'provider_capability_unsupported',
193
+ provider: 'claude-code',
194
+ capability: 'archive',
195
+ });
196
+ expect(mockMarkClaudeSessionArchived).not.toHaveBeenCalled();
197
+ expect(mockListNodeRecords).not.toHaveBeenCalled();
198
+ expect(mockDiscoverPortsWithMeta).not.toHaveBeenCalled();
199
+ expect(mockFetch).not.toHaveBeenCalled();
200
+ });
201
+
101
202
  it('does not misclassify non-404 local archive failures as session_not_found', async () => {
102
203
  const mockFetch = vi.fn(async () => new Response(JSON.stringify({ error: 'boom' }), { status: 500 }));
103
204
  vi.stubGlobal('fetch', mockFetch);