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
@@ -117,6 +117,8 @@ function createSession(overrides: Partial<OpencodeSession> & Pick<OpencodeSessio
117
117
  rawSessionId: overrides.rawSessionId,
118
118
  sourceSessionKey: overrides.sourceSessionKey ?? overrides.id,
119
119
  readOnly: overrides.readOnly,
120
+ provider: overrides.provider,
121
+ providerRawId: overrides.providerRawId,
120
122
  };
121
123
  }
122
124
 
@@ -164,7 +166,9 @@ describe('useOpencodeSync', () => {
164
166
  delete mockLocalStorage[key];
165
167
  },
166
168
  clear: () => {
167
- Object.keys(mockLocalStorage).forEach((key) => delete mockLocalStorage[key]);
169
+ for (const key of Object.keys(mockLocalStorage)) {
170
+ delete mockLocalStorage[key];
171
+ }
168
172
  },
169
173
  });
170
174
  (getSseStatusSnapshot() as Map<string, unknown>).clear();
@@ -384,4 +388,320 @@ describe('useOpencodeSync', () => {
384
388
 
385
389
  unmount();
386
390
  });
391
+
392
+ it('keeps recently-idle child sessions nested instead of removing them immediately', () => {
393
+ const eventTimestamp = 75_000;
394
+ const parentSession = createSession({
395
+ id: 'local:parent',
396
+ rawSessionId: 'parent',
397
+ hostId: 'local',
398
+ hostLabel: 'Local',
399
+ hostKind: 'local',
400
+ children: [
401
+ {
402
+ id: 'local:child',
403
+ slug: 'child',
404
+ title: 'Child Session',
405
+ directory: '/tmp/project',
406
+ projectName: 'Project',
407
+ parentID: 'local:parent',
408
+ time: { created: 1000, updated: 2000 },
409
+ realTimeStatus: 'busy',
410
+ waitingForUser: false,
411
+ },
412
+ ],
413
+ });
414
+
415
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
416
+ sessions: [parentSession],
417
+ });
418
+
419
+ act(() => {
420
+ eventSource.emitMessage({
421
+ type: 'session.status',
422
+ properties: {
423
+ sessionID: 'child',
424
+ status: { type: 'idle' },
425
+ },
426
+ timestamp: eventTimestamp,
427
+ });
428
+ });
429
+
430
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
431
+ const nextParent = data?.sessions.find((session) => session.id === 'local:parent');
432
+
433
+ expect(nextParent?.children).toHaveLength(1);
434
+ expect(nextParent?.children?.[0]?.id).toBe('local:child');
435
+ expect(nextParent?.children?.[0]?.realTimeStatus).toBe('idle');
436
+ expect(nextParent?.children?.[0]?.time.updated).toBe(eventTimestamp);
437
+
438
+ unmount();
439
+ });
440
+ });
441
+
442
+ describe('useOpencodeSync provider defaults', () => {
443
+ let mockLocalStorage: Record<string, string>;
444
+
445
+ beforeEach(() => {
446
+ mockLocalStorage = {};
447
+ MockEventSource.reset();
448
+ vi.useFakeTimers();
449
+ vi.stubGlobal('EventSource', MockEventSource as unknown as typeof EventSource);
450
+ vi.stubGlobal('localStorage', {
451
+ getItem: (key: string) => mockLocalStorage[key] || null,
452
+ setItem: (key: string, value: string) => {
453
+ mockLocalStorage[key] = value;
454
+ },
455
+ removeItem: (key: string) => {
456
+ delete mockLocalStorage[key];
457
+ },
458
+ clear: () => {
459
+ for (const key of Object.keys(mockLocalStorage)) {
460
+ delete mockLocalStorage[key];
461
+ }
462
+ },
463
+ });
464
+ (getSseStatusSnapshot() as Map<string, unknown>).clear();
465
+ });
466
+
467
+ afterEach(() => {
468
+ vi.clearAllTimers();
469
+ vi.useRealTimers();
470
+ vi.unstubAllGlobals();
471
+ MockEventSource.reset();
472
+ (getSseStatusSnapshot() as Map<string, unknown>).clear();
473
+ });
474
+
475
+ it('applies default provider opencode to new sessions from session.created event', () => {
476
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
477
+ sessions: [],
478
+ });
479
+
480
+ act(() => {
481
+ eventSource.emitMessage({
482
+ type: 'session.created',
483
+ properties: {
484
+ info: {
485
+ id: 'ses_1744181234567_build',
486
+ slug: 'session_1744181234567_build',
487
+ title: 'New Session',
488
+ directory: '/tmp/project',
489
+ time: { created: Date.now(), updated: Date.now() },
490
+ },
491
+ },
492
+ timestamp: Date.now(),
493
+ });
494
+ });
495
+
496
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
497
+ const newSession = data?.sessions.find((s) => s.id === 'local:ses_1744181234567_build');
498
+
499
+ expect(newSession?.provider).toBe('opencode');
500
+ expect(newSession?.readOnly).toBe(false);
501
+ expect(newSession?.capabilities).toEqual({
502
+ openProject: true,
503
+ openEditor: true,
504
+ archive: true,
505
+ delete: true,
506
+ });
507
+
508
+ unmount();
509
+ });
510
+
511
+ it('applies default provider opencode to existing sessions on update', () => {
512
+ const existingSession = createSession({
513
+ id: 'local:ses_123',
514
+ rawSessionId: 'ses_123',
515
+ hostId: 'local',
516
+ hostLabel: 'Local',
517
+ hostKind: 'local',
518
+ });
519
+
520
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
521
+ sessions: [existingSession],
522
+ });
523
+
524
+ act(() => {
525
+ eventSource.emitMessage({
526
+ type: 'session.updated',
527
+ properties: {
528
+ info: {
529
+ id: 'ses_123',
530
+ slug: 'session_123',
531
+ title: 'Updated Session',
532
+ directory: '/tmp/project',
533
+ time: { created: 1000, updated: Date.now() },
534
+ },
535
+ },
536
+ timestamp: Date.now(),
537
+ });
538
+ });
539
+
540
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
541
+ const updatedSession = data?.sessions.find((s) => s.id === 'local:ses_123');
542
+
543
+ expect(updatedSession?.provider).toBe('opencode');
544
+ expect(updatedSession?.readOnly).toBe(false);
545
+ expect(updatedSession?.capabilities).toEqual({
546
+ openProject: true,
547
+ openEditor: true,
548
+ archive: true,
549
+ delete: true,
550
+ });
551
+
552
+ unmount();
553
+ });
554
+
555
+ it('preserves explicit claude-code provider from event info', () => {
556
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
557
+ sessions: [],
558
+ });
559
+
560
+ act(() => {
561
+ eventSource.emitMessage({
562
+ type: 'session.created',
563
+ properties: {
564
+ info: {
565
+ id: 'claude~550e8400-e29b-41d4-a716-446655440000',
566
+ slug: 'claude_session',
567
+ title: 'Claude Session',
568
+ directory: '/tmp/claude-project',
569
+ time: { created: Date.now(), updated: Date.now() },
570
+ provider: 'claude-code',
571
+ providerRawId: '550e8400-e29b-41d4-a716-446655440000',
572
+ },
573
+ },
574
+ timestamp: Date.now(),
575
+ });
576
+ });
577
+
578
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
579
+ const newSession = data?.sessions.find((s) => s.id === 'local:claude~550e8400-e29b-41d4-a716-446655440000');
580
+
581
+ expect(newSession?.provider).toBe('claude-code');
582
+ expect(newSession?.providerRawId).toBe('550e8400-e29b-41d4-a716-446655440000');
583
+ expect(newSession?.capabilities).toEqual({
584
+ openProject: true,
585
+ openEditor: false,
586
+ archive: true,
587
+ delete: true,
588
+ });
589
+
590
+ unmount();
591
+ });
592
+
593
+ it('preserves explicit readOnly true when specified', () => {
594
+ const existingSession = createSession({
595
+ id: 'local:ses_123',
596
+ rawSessionId: 'ses_123',
597
+ hostId: 'local',
598
+ readOnly: true,
599
+ });
600
+
601
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
602
+ sessions: [existingSession],
603
+ });
604
+
605
+ act(() => {
606
+ eventSource.emitMessage({
607
+ type: 'session.status',
608
+ properties: {
609
+ sessionID: 'ses_123',
610
+ status: { type: 'busy' },
611
+ },
612
+ timestamp: Date.now(),
613
+ });
614
+ });
615
+
616
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
617
+ const updatedSession = data?.sessions.find((s) => s.id === 'local:ses_123');
618
+
619
+ expect(updatedSession?.readOnly).toBe(true);
620
+
621
+ unmount();
622
+ });
623
+
624
+ it('propagates provider and readOnly on session.status events for existing sessions', () => {
625
+ const localSession = createSession({
626
+ id: 'local:abc',
627
+ rawSessionId: 'abc',
628
+ hostId: 'local',
629
+ provider: 'claude-code',
630
+ readOnly: true,
631
+ });
632
+
633
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
634
+ sessions: [localSession],
635
+ });
636
+
637
+ act(() => {
638
+ eventSource.emitMessage({
639
+ type: 'session.status',
640
+ properties: {
641
+ sessionID: 'abc',
642
+ status: { type: 'busy' },
643
+ },
644
+ timestamp: Date.now(),
645
+ });
646
+ });
647
+
648
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
649
+ const updatedSession = data?.sessions.find((s) => s.id === 'local:abc');
650
+
651
+ expect(updatedSession?.provider).toBe('claude-code');
652
+ expect(updatedSession?.readOnly).toBe(true);
653
+
654
+ unmount();
655
+ });
656
+
657
+ it('keeps OpenCode SSE updates scoped to plain local ids when a Claude session shares the same raw uuid', () => {
658
+ const sharedUuid = '550e8400-e29b-41d4-a716-446655440000';
659
+ const openCodeSession = createSession({
660
+ id: `local:${sharedUuid}`,
661
+ rawSessionId: sharedUuid,
662
+ hostId: 'local',
663
+ hostLabel: 'Local',
664
+ hostKind: 'local',
665
+ provider: 'opencode',
666
+ readOnly: false,
667
+ });
668
+ const claudeSession = createSession({
669
+ id: `local:claude~${sharedUuid}`,
670
+ rawSessionId: sharedUuid,
671
+ hostId: 'local',
672
+ hostLabel: 'Local',
673
+ hostKind: 'local',
674
+ provider: 'claude-code',
675
+ providerRawId: sharedUuid,
676
+ readOnly: true,
677
+ });
678
+
679
+ const { eventSource, queryClient, queryKey, unmount } = renderUseOpencodeSync({
680
+ sessions: [openCodeSession, claudeSession],
681
+ });
682
+
683
+ act(() => {
684
+ eventSource.emitMessage({
685
+ type: 'session.status',
686
+ properties: {
687
+ sessionID: sharedUuid,
688
+ status: { type: 'busy' },
689
+ },
690
+ timestamp: Date.now(),
691
+ });
692
+ });
693
+
694
+ const data = queryClient.getQueryData<SessionsQueryData>(queryKey);
695
+ const updatedOpenCodeSession = data?.sessions.find((s) => s.id === `local:${sharedUuid}`);
696
+ const updatedClaudeSession = data?.sessions.find((s) => s.id === `local:claude~${sharedUuid}`);
697
+
698
+ expect(updatedOpenCodeSession?.provider).toBe('opencode');
699
+ expect(updatedOpenCodeSession?.realTimeStatus).toBe('busy');
700
+ expect(updatedClaudeSession?.provider).toBe('claude-code');
701
+ expect(updatedClaudeSession?.realTimeStatus).toBe('idle');
702
+ expect(getSseStatusSnapshot().get(`local:${sharedUuid}`)?.status).toBe('busy');
703
+ expect(getSseStatusSnapshot().has(`local:claude~${sharedUuid}`)).toBe(false);
704
+
705
+ unmount();
706
+ });
387
707
  });
@@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
5
5
  import { OpencodeEvent, OpencodeSession } from '@/types';
6
6
  import { playAlertSound, playAttentionSound } from '@/lib/notificationSound';
7
7
  import { composeSourceKey, getSessionIdFromSourceKey } from '@/lib/hostIdentity';
8
+ import { DEFAULT_PROVIDER_CONTEXT, getDefaultProviderContext } from '@/lib/session-providers/providerIds';
8
9
 
9
10
  const WAITING_STORAGE_KEY = 'vibepulse:waiting-sessions:v2';
10
11
  const WAITING_ENTER_DELAY_MS = 1500;
@@ -104,6 +105,7 @@ function toSourceKey(hostId: string, sessionId: string): string {
104
105
  }
105
106
 
106
107
  function normalizeSessionForSource(info: OpencodeSession, source: EventSourceContext): OpencodeSession {
108
+ const providerDefaults = getDefaultProviderContext(info.provider ?? DEFAULT_PROVIDER_CONTEXT.provider);
107
109
  const rawSessionId = getSessionIdFromSourceKey(info.rawSessionId ?? info.id) ?? info.rawSessionId ?? info.id;
108
110
  const sourceSessionKey = composeSourceKey(source.hostId, rawSessionId);
109
111
  const rawParentId = info.parentID ? getSessionIdFromSourceKey(info.parentID) ?? info.parentID : info.parentID;
@@ -118,7 +120,10 @@ function normalizeSessionForSource(info: OpencodeSession, source: EventSourceCon
118
120
  hostBaseUrl: source.hostBaseUrl,
119
121
  rawSessionId,
120
122
  sourceSessionKey,
121
- readOnly: false,
123
+ readOnly: info.readOnly ?? providerDefaults.readOnly,
124
+ capabilities: info.capabilities ?? providerDefaults.capabilities,
125
+ provider: info.provider ?? providerDefaults.provider,
126
+ providerRawId: info.providerRawId ?? rawSessionId,
122
127
  children: info.children?.map((child) =>
123
128
  normalizeSessionForSource({
124
129
  ...child,
@@ -354,19 +359,24 @@ export function useOpencodeSync() {
354
359
  }
355
360
 
356
361
  const applyEvent = (s: OpencodeSession): OpencodeSession => {
362
+ const providerDefaults = getDefaultProviderContext(s.provider ?? DEFAULT_PROVIDER_CONTEXT.provider);
357
363
  const baseSession: OpencodeSession = {
358
364
  ...s,
359
365
  hostId: source.hostId,
360
366
  hostLabel: source.hostLabel,
361
367
  hostKind: source.hostKind,
362
368
  hostBaseUrl: source.hostBaseUrl ?? s.hostBaseUrl,
363
- readOnly: false,
369
+ readOnly: s.readOnly ?? providerDefaults.readOnly,
370
+ capabilities: s.capabilities ?? providerDefaults.capabilities,
371
+ provider: s.provider ?? providerDefaults.provider,
372
+ providerRawId: s.providerRawId ?? s.rawSessionId,
364
373
  };
365
374
 
366
375
  switch (event.type) {
367
376
  case 'session.status': {
368
377
  const statusType = event.properties?.status?.type as 'idle' | 'busy' | 'retry' | undefined;
369
378
  if (!statusType) return baseSession;
379
+ const statusTimestamp = typeof event.timestamp === 'number' ? event.timestamp : Date.now();
370
380
  recordSseStatus(s.id, statusType);
371
381
  const isParentSession = !s.parentID;
372
382
  const shouldAutoUnarchive = statusType === 'busy' || statusType === 'retry';
@@ -393,7 +403,9 @@ export function useOpencodeSync() {
393
403
  }
394
404
  return {
395
405
  ...baseSession,
396
- time: shouldAutoUnarchive ? { ...(s.time || {}), archived: undefined } : s.time,
406
+ time: shouldAutoUnarchive
407
+ ? { ...(s.time || {}), updated: statusTimestamp, archived: undefined }
408
+ : { ...(s.time || {}), updated: statusTimestamp },
397
409
  realTimeStatus: statusType,
398
410
  waitingForUser:
399
411
  statusType === 'retry'
@@ -448,15 +460,7 @@ export function useOpencodeSync() {
448
460
  }
449
461
  if (session.children?.some(c => c.id === sourceSessionId)) {
450
462
  found = true;
451
- // If the event is a status update to 'idle', we should filter the child out
452
- // so it disappears from the UI without needing a full refetch, matching backend logic.
453
- if (event.type === 'session.status' && event.properties?.status?.type === 'idle') {
454
- return {
455
- ...session,
456
- children: session.children.filter(c => c.id !== sourceSessionId)
457
- };
458
- }
459
-
463
+
460
464
  return {
461
465
  ...session,
462
466
  children: session.children.map(c => c.id === sourceSessionId ? applyEvent(c) : c)
@@ -0,0 +1,75 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { mkdtemp, readFile, rm } from 'fs/promises';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ async function withTempHome<T>(fn: (homeDir: string) => Promise<T>): Promise<T> {
7
+ const homeDir = await mkdtemp(join(tmpdir(), 'vibepulse-claude-overrides-'));
8
+ const originalHome = process.env.HOME;
9
+ process.env.HOME = homeDir;
10
+ try {
11
+ return await fn(homeDir);
12
+ } finally {
13
+ if (originalHome === undefined) delete process.env.HOME;
14
+ else process.env.HOME = originalHome;
15
+ await rm(homeDir, { recursive: true, force: true });
16
+ }
17
+ }
18
+
19
+ describe('claudeSessionOverrides', () => {
20
+ it('persists archived and deleted Claude overrides to the local registry file', async () => {
21
+ await withTempHome(async (homeDir) => {
22
+ const mod = await import('./claudeSessionOverrides');
23
+ await mod.markClaudeSessionArchived('session-a', 100);
24
+ await mod.markClaudeSessionDeleted('session-b', 200);
25
+
26
+ const entries = await mod.listClaudeSessionOverrides();
27
+ expect(entries).toEqual(expect.arrayContaining([
28
+ expect.objectContaining({ sessionId: 'session-a', archivedAt: 100 }),
29
+ expect.objectContaining({ sessionId: 'session-b', deletedAt: 200 }),
30
+ ]));
31
+
32
+ const content = await readFile(join(homeDir, '.config', 'vibepulse', 'claude-session-overrides.jsonc'), 'utf-8');
33
+ expect(content).toContain('session-a');
34
+ expect(content).toContain('session-b');
35
+ });
36
+ });
37
+
38
+ it('preserves all entries under concurrent override writes', async () => {
39
+ await withTempHome(async () => {
40
+ const mod = await import('./claudeSessionOverrides');
41
+
42
+ await Promise.all([
43
+ mod.markClaudeSessionArchived('session-a', 100),
44
+ mod.markClaudeSessionDeleted('session-b', 200),
45
+ mod.markClaudeSessionArchived('session-c', 300),
46
+ ]);
47
+
48
+ const entries = await mod.listClaudeSessionOverrides();
49
+ expect(entries).toEqual(expect.arrayContaining([
50
+ expect.objectContaining({ sessionId: 'session-a', archivedAt: 100 }),
51
+ expect.objectContaining({ sessionId: 'session-b', deletedAt: 200 }),
52
+ expect.objectContaining({ sessionId: 'session-c', archivedAt: 300 }),
53
+ ]));
54
+ expect(entries).toHaveLength(3);
55
+ });
56
+ });
57
+
58
+ it('can clear archived state while preserving deleted state when restoring a Claude session', async () => {
59
+ await withTempHome(async () => {
60
+ const mod = await import('./claudeSessionOverrides');
61
+ await mod.markClaudeSessionArchived('session-a', 100);
62
+ await mod.markClaudeSessionDeleted('session-b', 200);
63
+ await mod.clearClaudeSessionArchived('session-a');
64
+ await mod.clearClaudeSessionArchived('session-b');
65
+
66
+ const entries = await mod.listClaudeSessionOverrides();
67
+ expect(entries).toEqual(expect.arrayContaining([
68
+ expect.objectContaining({ sessionId: 'session-b', deletedAt: 200, restoredAt: expect.any(Number) }),
69
+ ]));
70
+ expect(entries).toEqual(expect.arrayContaining([
71
+ expect.objectContaining({ sessionId: 'session-a', restoredAt: expect.any(Number) }),
72
+ ]));
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,169 @@
1
+ import { existsSync, mkdirSync } from 'fs';
2
+ import { readFile, rename, writeFile } from 'fs/promises';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import { parse, stringify } from 'comment-json';
6
+
7
+ export const VIBEPULSE_CONFIG_DIR = join(homedir(), '.config', 'vibepulse');
8
+ export const CLAUDE_SESSION_OVERRIDES_PATH = join(VIBEPULSE_CONFIG_DIR, 'claude-session-overrides.jsonc');
9
+
10
+ export interface ClaudeSessionOverrideEntry {
11
+ sessionId: string;
12
+ archivedAt?: number;
13
+ deletedAt?: number;
14
+ restoredAt?: number;
15
+ updatedAt: number;
16
+ }
17
+
18
+ interface ClaudeSessionOverridesFile {
19
+ version: number;
20
+ sessions: ClaudeSessionOverrideEntry[];
21
+ }
22
+
23
+ let claudeOverrideWriteQueue: Promise<void> = Promise.resolve();
24
+
25
+ function ensureConfigDir(): void {
26
+ if (!existsSync(VIBEPULSE_CONFIG_DIR)) {
27
+ mkdirSync(VIBEPULSE_CONFIG_DIR, { recursive: true });
28
+ }
29
+ }
30
+
31
+ function defaultOverrides(): ClaudeSessionOverridesFile {
32
+ return {
33
+ version: 1,
34
+ sessions: [],
35
+ };
36
+ }
37
+
38
+ function normalizeEntry(entry: unknown): ClaudeSessionOverrideEntry | null {
39
+ if (!entry || typeof entry !== 'object') return null;
40
+ const candidate = entry as Record<string, unknown>;
41
+ if (typeof candidate.sessionId !== 'string' || typeof candidate.updatedAt !== 'number') return null;
42
+ if (candidate.archivedAt !== undefined && typeof candidate.archivedAt !== 'number') return null;
43
+ if (candidate.deletedAt !== undefined && typeof candidate.deletedAt !== 'number') return null;
44
+ if (candidate.restoredAt !== undefined && typeof candidate.restoredAt !== 'number') return null;
45
+
46
+ return {
47
+ sessionId: candidate.sessionId,
48
+ updatedAt: candidate.updatedAt,
49
+ ...(typeof candidate.archivedAt === 'number' ? { archivedAt: candidate.archivedAt } : {}),
50
+ ...(typeof candidate.deletedAt === 'number' ? { deletedAt: candidate.deletedAt } : {}),
51
+ ...(typeof candidate.restoredAt === 'number' ? { restoredAt: candidate.restoredAt } : {}),
52
+ };
53
+ }
54
+
55
+ async function readOverridesFile(): Promise<ClaudeSessionOverridesFile> {
56
+ try {
57
+ ensureConfigDir();
58
+ if (!existsSync(CLAUDE_SESSION_OVERRIDES_PATH)) {
59
+ const initial = defaultOverrides();
60
+ await writeOverridesFile(initial);
61
+ return initial;
62
+ }
63
+
64
+ const raw = await readFile(CLAUDE_SESSION_OVERRIDES_PATH, 'utf-8');
65
+ const parsed = parse(raw, null, false) as unknown;
66
+ if (!parsed || typeof parsed !== 'object') {
67
+ return defaultOverrides();
68
+ }
69
+
70
+ const file = parsed as Record<string, unknown>;
71
+ const sessions = Array.isArray(file.sessions)
72
+ ? file.sessions.map(normalizeEntry).filter((entry): entry is ClaudeSessionOverrideEntry => entry !== null)
73
+ : [];
74
+
75
+ return {
76
+ version: typeof file.version === 'number' ? file.version : 1,
77
+ sessions,
78
+ };
79
+ } catch {
80
+ return defaultOverrides();
81
+ }
82
+ }
83
+
84
+ async function writeOverridesFile(file: ClaudeSessionOverridesFile): Promise<void> {
85
+ ensureConfigDir();
86
+ const tempPath = `${CLAUDE_SESSION_OVERRIDES_PATH}.tmp`;
87
+ await writeFile(tempPath, stringify(file, null, 2), 'utf-8');
88
+ await rename(tempPath, CLAUDE_SESSION_OVERRIDES_PATH);
89
+ }
90
+
91
+ export async function listClaudeSessionOverrides(): Promise<ClaudeSessionOverrideEntry[]> {
92
+ const file = await readOverridesFile();
93
+ return file.sessions;
94
+ }
95
+
96
+ export async function getClaudeSessionOverride(sessionId: string): Promise<ClaudeSessionOverrideEntry | null> {
97
+ const file = await readOverridesFile();
98
+ return file.sessions.find((entry) => entry.sessionId === sessionId) ?? null;
99
+ }
100
+
101
+ async function upsertClaudeSessionOverride(
102
+ sessionId: string,
103
+ mutate: (current: ClaudeSessionOverrideEntry | null, now: number) => ClaudeSessionOverrideEntry | null,
104
+ ): Promise<void> {
105
+ const run = async () => {
106
+ const file = await readOverridesFile();
107
+ const now = Date.now();
108
+ const current = file.sessions.find((entry) => entry.sessionId === sessionId) ?? null;
109
+ const next = mutate(current, now);
110
+ const withoutCurrent = file.sessions.filter((entry) => entry.sessionId !== sessionId);
111
+ file.sessions = next ? [...withoutCurrent, next] : withoutCurrent;
112
+ await writeOverridesFile(file);
113
+ };
114
+
115
+ const queued = claudeOverrideWriteQueue.then(run, run);
116
+ claudeOverrideWriteQueue = queued.then(() => undefined, () => undefined);
117
+ await queued;
118
+ }
119
+
120
+ export async function markClaudeSessionArchived(sessionId: string, archivedAt: number = Date.now()): Promise<void> {
121
+ await upsertClaudeSessionOverride(sessionId, (current, now) => ({
122
+ sessionId,
123
+ archivedAt,
124
+ deletedAt: current?.deletedAt,
125
+ restoredAt: undefined,
126
+ updatedAt: now,
127
+ }));
128
+ }
129
+
130
+ export async function markClaudeSessionDeleted(sessionId: string, deletedAt: number = Date.now()): Promise<void> {
131
+ await upsertClaudeSessionOverride(sessionId, (_current, now) => ({
132
+ sessionId,
133
+ deletedAt,
134
+ restoredAt: undefined,
135
+ updatedAt: now,
136
+ }));
137
+ }
138
+
139
+ export async function clearClaudeSessionDeleted(sessionId: string): Promise<void> {
140
+ await upsertClaudeSessionOverride(sessionId, (current, now) => {
141
+ if (!current) return null;
142
+ if (current.archivedAt === undefined) return null;
143
+ return {
144
+ sessionId,
145
+ archivedAt: current.archivedAt,
146
+ restoredAt: current.restoredAt,
147
+ updatedAt: now,
148
+ };
149
+ });
150
+ }
151
+
152
+ export async function clearClaudeSessionArchived(sessionId: string): Promise<void> {
153
+ await upsertClaudeSessionOverride(sessionId, (current, now) => {
154
+ if (!current) return null;
155
+ if (current.deletedAt !== undefined) {
156
+ return {
157
+ sessionId,
158
+ deletedAt: current.deletedAt,
159
+ restoredAt: now,
160
+ updatedAt: now,
161
+ };
162
+ }
163
+ return {
164
+ sessionId,
165
+ restoredAt: now,
166
+ updatedAt: now,
167
+ };
168
+ });
169
+ }