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
@@ -22,6 +22,16 @@ interface ProjectCardProps {
22
22
  multipleHostsEnabled?: boolean;
23
23
  }
24
24
 
25
+ const RECENT_IDLE_CHILD_VISIBILITY_WINDOW_MS = 60_000;
26
+
27
+ function shouldShowChildSession(child: { realTimeStatus: string; waitingForUser: boolean; updatedAt: number }): boolean {
28
+ if (child.realTimeStatus !== 'idle' || child.waitingForUser) {
29
+ return true;
30
+ }
31
+
32
+ return Date.now() - child.updatedAt <= RECENT_IDLE_CHILD_VISIBILITY_WINDOW_MS;
33
+ }
34
+
25
35
  function formatRelativeTime(timestamp: number): string {
26
36
  const diffMs = Date.now() - timestamp;
27
37
  const diffMins = Math.floor(diffMs / (1000 * 60));
@@ -38,36 +48,39 @@ function buildTooltipTitle(lines: string[], debugReason?: string): string {
38
48
  return debugReason ? [...lines, `Reason: ${debugReason}`].join('\n') : lines.join('\n');
39
49
  }
40
50
 
41
- function StatusDot({ status, waitingForUser }: { status: string; waitingForUser: boolean }) {
51
+ function StatusDot({ status, waitingForUser, provider }: { status: string; waitingForUser: boolean; provider?: string }) {
52
+ const isClaudeProvider = provider === 'claude-code';
53
+ const shapeClass = isClaudeProvider ? 'rotate-45 rounded-[1px]' : 'rounded-full';
54
+ const dotSizeClass = isClaudeProvider ? 'h-[7px] w-[7px]' : 'h-2 w-2';
42
55
  if (waitingForUser) {
43
56
  return (
44
- <span className="relative flex h-2 w-2 flex-shrink-0" title="Waiting">
45
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
46
- <span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500"></span>
57
+ <span className={`relative flex ${dotSizeClass} flex-shrink-0`} title="Waiting">
58
+ <span className={`animate-ping absolute inline-flex h-full w-full ${shapeClass} bg-amber-400 opacity-75`}></span>
59
+ <span className={`relative inline-flex ${dotSizeClass} ${shapeClass} bg-amber-500`}></span>
47
60
  </span>
48
61
  );
49
62
  }
50
63
  switch (status) {
51
64
  case 'busy':
52
65
  return (
53
- <span className="relative flex h-2 w-2 flex-shrink-0" title="Running">
54
- <span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
55
- <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
66
+ <span className={`relative flex ${dotSizeClass} flex-shrink-0`} title="Running">
67
+ <span className={`animate-pulse absolute inline-flex h-full w-full ${shapeClass} bg-emerald-400 opacity-75`}></span>
68
+ <span className={`relative inline-flex ${dotSizeClass} ${shapeClass} bg-emerald-500`}></span>
56
69
  </span>
57
70
  );
58
71
  case 'retry':
59
72
  return (
60
- <span className="relative flex h-2 w-2 flex-shrink-0" title="Retrying">
61
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
62
- <span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
73
+ <span className={`relative flex ${dotSizeClass} flex-shrink-0`} title="Retrying">
74
+ <span className={`animate-ping absolute inline-flex h-full w-full ${shapeClass} bg-red-400 opacity-75`}></span>
75
+ <span className={`relative inline-flex ${dotSizeClass} ${shapeClass} bg-red-500`}></span>
63
76
  </span>
64
77
  );
65
78
  default:
66
- return <span className="inline-flex rounded-full h-2 w-2 bg-gray-400 flex-shrink-0" title="Idle"></span>;
79
+ return <span className={`inline-flex ${dotSizeClass} bg-gray-400 flex-shrink-0 ${shapeClass}`} title="Idle"></span>;
67
80
  }
68
81
  }
69
82
 
70
- function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionError, onPendingActionChange }: { cards: KanbanCard[]; readOnly?: boolean; isActionPending: boolean; onActionError: (message: string | null) => void; onPendingActionChange: (value: 'open' | 'archive' | 'delete' | null) => void }) {
83
+ function HeaderActionMenu({ cards, isActionPending, onActionError, onPendingActionChange, readOnlyMode }: { cards: KanbanCard[]; isActionPending: boolean; onActionError: (message: string | null) => void; onPendingActionChange: (value: 'open' | 'archive' | 'restore' | 'delete' | null) => void; readOnlyMode: boolean }) {
71
84
  const queryClient = useQueryClient();
72
85
  const [open, setOpen] = useState(false);
73
86
  const menuRef = useRef<HTMLDivElement>(null);
@@ -83,7 +96,11 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
83
96
  return () => document.removeEventListener('mousedown', handler);
84
97
  }, [open]);
85
98
 
86
- const hasUnarchived = cards.some(c => c.status !== 'done');
99
+ const canArchiveAny = !readOnlyMode && cards.some(c => c.capabilities ? c.capabilities.archive : !c.readOnly);
100
+ const canDeleteAny = !readOnlyMode && cards.some(c => c.capabilities ? c.capabilities.delete : !c.readOnly);
101
+
102
+ if (!canArchiveAny && !canDeleteAny) return null;
103
+
87
104
  const hasMixedHosts = new Set(cards.map((card) => card.hostId ?? 'local')).size > 1;
88
105
 
89
106
  const handleArchiveAll = async (e: React.MouseEvent) => {
@@ -96,7 +113,15 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
96
113
  return;
97
114
  }
98
115
 
99
- const unarchivedCards = cards.filter(c => c.status !== 'done');
116
+ const unarchivedCards = cards.filter(c => {
117
+ if (c.status === 'done') return false;
118
+ return c.capabilities ? c.capabilities.archive : !c.readOnly;
119
+ });
120
+ if (unarchivedCards.length === 0) {
121
+ setOpen(false);
122
+ return;
123
+ }
124
+
100
125
  onPendingActionChange('archive');
101
126
  try {
102
127
  const responses = await Promise.all(unarchivedCards.map(card =>
@@ -116,6 +141,41 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
116
141
  await queryClient.invalidateQueries({ queryKey: ['sessions'] });
117
142
  };
118
143
 
144
+ const handleRestoreAll = async (e: React.MouseEvent) => {
145
+ e.stopPropagation();
146
+ if (isActionPending) return;
147
+ onActionError(null);
148
+ if (hasMixedHosts) {
149
+ onActionError('Mixed-host restore is not supported');
150
+ setOpen(false);
151
+ return;
152
+ }
153
+
154
+ const archivedCards = cards.filter(c => c.status === 'done' && (c.capabilities ? c.capabilities.archive : !c.readOnly));
155
+ if (archivedCards.length === 0) {
156
+ setOpen(false);
157
+ return;
158
+ }
159
+
160
+ onPendingActionChange('restore');
161
+ try {
162
+ const responses = await Promise.all(archivedCards.map(card =>
163
+ fetch(`/api/sessions/${card.id}/restore`, { method: 'POST' })
164
+ ));
165
+ const failedResponse = responses.find((response) => !response.ok);
166
+ if (failedResponse) {
167
+ const errorBody = await failedResponse.json().catch(() => null);
168
+ onActionError(mapSessionActionError(errorBody, 'Failed to restore sessions'));
169
+ }
170
+ } catch {
171
+ onActionError('Remote node is offline or unreachable.');
172
+ } finally {
173
+ onPendingActionChange(null);
174
+ }
175
+ setOpen(false);
176
+ await queryClient.invalidateQueries({ queryKey: ['sessions'] });
177
+ };
178
+
119
179
  const handleDeleteAll = async (e: React.MouseEvent) => {
120
180
  e.stopPropagation();
121
181
  if (isActionPending) return;
@@ -126,10 +186,16 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
126
186
  return;
127
187
  }
128
188
 
129
- if (!confirm(`Delete ${cards.length} session(s)? This cannot be undone.`)) return;
189
+ const deletableCards = cards.filter(c => c.capabilities ? c.capabilities.delete : !c.readOnly);
190
+ if (deletableCards.length === 0) {
191
+ setOpen(false);
192
+ return;
193
+ }
194
+
195
+ if (!confirm(`Delete ${deletableCards.length} session(s)? This cannot be undone.`)) return;
130
196
  onPendingActionChange('delete');
131
197
  try {
132
- const responses = await Promise.all(cards.map(card =>
198
+ const responses = await Promise.all(deletableCards.map(card =>
133
199
  fetch(`/api/sessions/${card.id}/delete`, { method: 'POST' })
134
200
  ));
135
201
  const failedResponse = responses.find((response) => !response.ok);
@@ -146,7 +212,8 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
146
212
  await queryClient.invalidateQueries({ queryKey: ['sessions'] });
147
213
  };
148
214
 
149
- if (readOnly) return null;
215
+ const hasArchivableInBatch = !readOnlyMode && cards.some(c => (c.status !== 'done') && (c.capabilities ? c.capabilities.archive : !c.readOnly));
216
+ const hasRestorableInBatch = !readOnlyMode && cards.some(c => c.status === 'done' && (c.capabilities ? c.capabilities.archive : !c.readOnly));
150
217
 
151
218
  return (
152
219
  <div className="relative" ref={menuRef}>
@@ -163,7 +230,7 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
163
230
  </button>
164
231
  {open && (
165
232
  <div className="absolute right-0 top-6 w-32 rounded-md border border-gray-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900 z-20">
166
- {hasUnarchived && (
233
+ {hasArchivableInBatch && (
167
234
  <button
168
235
  type="button"
169
236
  className="w-full text-left px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-zinc-800"
@@ -173,14 +240,26 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
173
240
  Archive all
174
241
  </button>
175
242
  )}
176
- <button
177
- type="button"
178
- className="w-full text-left px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
179
- onClick={handleDeleteAll}
180
- disabled={isActionPending}
181
- >
182
- Delete all
183
- </button>
243
+ {hasRestorableInBatch && (
244
+ <button
245
+ type="button"
246
+ className="w-full text-left px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-zinc-800"
247
+ onClick={handleRestoreAll}
248
+ disabled={isActionPending}
249
+ >
250
+ Restore all
251
+ </button>
252
+ )}
253
+ {canDeleteAny && (
254
+ <button
255
+ type="button"
256
+ className="w-full text-left px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
257
+ onClick={handleDeleteAll}
258
+ disabled={isActionPending}
259
+ >
260
+ Delete all
261
+ </button>
262
+ )}
184
263
  </div>
185
264
  )}
186
265
  </div>
@@ -188,7 +267,7 @@ function HeaderActionMenu({ cards, readOnly = false, isActionPending, onActionEr
188
267
  }
189
268
 
190
269
  // Hover-reveal action menu for each session row
191
- function RowActionMenu({ cardId, archived, isActionPending, onActionError, onPendingActionChange }: { cardId: string; archived: boolean; isActionPending: boolean; onActionError: (message: string | null) => void; onPendingActionChange: (value: 'open' | 'archive' | 'delete' | null) => void }) {
270
+ function RowActionMenu({ card, isActionPending, onActionError, onPendingActionChange, readOnlyMode }: { card: KanbanCard; isActionPending: boolean; onActionError: (message: string | null) => void; onPendingActionChange: (value: 'open' | 'archive' | 'restore' | 'delete' | null) => void; readOnlyMode: boolean }) {
192
271
  const queryClient = useQueryClient();
193
272
  const [open, setOpen] = useState(false);
194
273
  const menuRef = useRef<HTMLDivElement>(null);
@@ -204,13 +283,18 @@ function RowActionMenu({ cardId, archived, isActionPending, onActionError, onPen
204
283
  return () => document.removeEventListener('mousedown', handler);
205
284
  }, [open]);
206
285
 
286
+ const canArchive = !readOnlyMode && (card.capabilities ? card.capabilities.archive : !card.readOnly);
287
+ const canDelete = !readOnlyMode && (card.capabilities ? card.capabilities.delete : !card.readOnly);
288
+
289
+ if (!canArchive && !canDelete) return null;
290
+
207
291
  const handleArchive = async (e: React.MouseEvent) => {
208
292
  e.stopPropagation();
209
293
  if (isActionPending) return;
210
294
  onActionError(null);
211
295
  onPendingActionChange('archive');
212
296
  try {
213
- const response = await fetch(`/api/sessions/${cardId}/archive`, { method: 'POST' });
297
+ const response = await fetch(`/api/sessions/${card.id}/archive`, { method: 'POST' });
214
298
  if (!response.ok) {
215
299
  const errorBody = await response.json().catch(() => null);
216
300
  onActionError(mapSessionActionError(errorBody, 'Failed to archive session'));
@@ -224,13 +308,33 @@ function RowActionMenu({ cardId, archived, isActionPending, onActionError, onPen
224
308
  }
225
309
  };
226
310
 
311
+ const handleRestore = async (e: React.MouseEvent) => {
312
+ e.stopPropagation();
313
+ if (isActionPending) return;
314
+ onActionError(null);
315
+ onPendingActionChange('restore');
316
+ try {
317
+ const response = await fetch(`/api/sessions/${card.id}/restore`, { method: 'POST' });
318
+ if (!response.ok) {
319
+ const errorBody = await response.json().catch(() => null);
320
+ onActionError(mapSessionActionError(errorBody, 'Failed to restore session'));
321
+ }
322
+ } catch {
323
+ onActionError('Remote node is offline or unreachable.');
324
+ } finally {
325
+ onPendingActionChange(null);
326
+ setOpen(false);
327
+ await queryClient.invalidateQueries({ queryKey: ['sessions'] });
328
+ }
329
+ };
330
+
227
331
  const handleDelete = async (e: React.MouseEvent) => {
228
332
  e.stopPropagation();
229
333
  if (isActionPending) return;
230
334
  onActionError(null);
231
335
  onPendingActionChange('delete');
232
336
  try {
233
- const response = await fetch(`/api/sessions/${cardId}/delete`, { method: 'POST' });
337
+ const response = await fetch(`/api/sessions/${card.id}/delete`, { method: 'POST' });
234
338
  if (!response.ok) {
235
339
  const errorBody = await response.json().catch(() => null);
236
340
  onActionError(mapSessionActionError(errorBody, 'Failed to delete session'));
@@ -259,7 +363,7 @@ function RowActionMenu({ cardId, archived, isActionPending, onActionError, onPen
259
363
  </button>
260
364
  {open && (
261
365
  <div className="absolute right-0 top-6 w-28 rounded-md border border-gray-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900 z-20">
262
- {!archived ? (
366
+ {(card.status !== 'done' && canArchive) && (
263
367
  <button
264
368
  type="button"
265
369
  className="w-full text-left px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-zinc-800"
@@ -268,15 +372,27 @@ function RowActionMenu({ cardId, archived, isActionPending, onActionError, onPen
268
372
  >
269
373
  Archive
270
374
  </button>
271
- ) : null}
272
- <button
273
- type="button"
274
- className="w-full text-left px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
275
- onClick={handleDelete}
276
- disabled={isActionPending}
277
- >
278
- Delete
279
- </button>
375
+ )}
376
+ {(card.status === 'done' && canArchive) && (
377
+ <button
378
+ type="button"
379
+ className="w-full text-left px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-zinc-800"
380
+ onClick={handleRestore}
381
+ disabled={isActionPending}
382
+ >
383
+ Restore
384
+ </button>
385
+ )}
386
+ {canDelete && (
387
+ <button
388
+ type="button"
389
+ className="w-full text-left px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
390
+ onClick={handleDelete}
391
+ disabled={isActionPending}
392
+ >
393
+ Delete
394
+ </button>
395
+ )}
280
396
  </div>
281
397
  )}
282
398
  </div>
@@ -284,10 +400,10 @@ function RowActionMenu({ cardId, archived, isActionPending, onActionError, onPen
284
400
  }
285
401
 
286
402
  // Session row with expandable subagent children
287
- function SessionRow({ card, isLast, readOnly = false, isActionPending, onActionError, onPendingActionChange }: { card: KanbanCard; isLast: boolean; readOnly?: boolean; isActionPending: boolean; onActionError: (message: string | null) => void; onPendingActionChange: (value: 'open' | 'archive' | 'delete' | null) => void }) {
403
+ function SessionRow({ card, isLast, isActionPending, onActionError, onPendingActionChange, readOnlyMode }: { card: KanbanCard; isLast: boolean; isActionPending: boolean; onActionError: (message: string | null) => void; onPendingActionChange: (value: 'open' | 'archive' | 'restore' | 'delete' | null) => void; readOnlyMode: boolean }) {
288
404
  const [expanded, setExpanded] = useState(true);
289
405
  const visibleChildren = (card.children || []).filter(
290
- (child) => child.realTimeStatus !== 'idle' || child.waitingForUser
406
+ shouldShowChildSession
291
407
  );
292
408
  const hasChildren = visibleChildren.length > 0;
293
409
  const rowTitle = buildTooltipTitle([
@@ -319,7 +435,7 @@ function SessionRow({ card, isLast, readOnly = false, isActionPending, onActionE
319
435
  </svg>
320
436
  </button>
321
437
  )}
322
- <StatusDot status={card.opencodeStatus} waitingForUser={card.waitingForUser} />
438
+ <StatusDot status={card.opencodeStatus} waitingForUser={card.waitingForUser} provider={card.provider} />
323
439
  <span className="text-sm text-gray-700 dark:text-gray-300 truncate flex-1 min-w-0">
324
440
  {card.title || 'Untitled Session'}
325
441
  </span>
@@ -334,11 +450,9 @@ function SessionRow({ card, isLast, readOnly = false, isActionPending, onActionE
334
450
  {formatRelativeTime(card.updatedAt)}
335
451
  </span>
336
452
  {/* Action menu: hidden by default, visible on hover */}
337
- {!readOnly ? (
338
- <div className="hidden group-hover/row:flex flex-shrink-0">
339
- <RowActionMenu cardId={card.id} archived={card.status === 'done'} isActionPending={isActionPending} onActionError={onActionError} onPendingActionChange={onPendingActionChange} />
340
- </div>
341
- ) : null}
453
+ <div className="hidden group-hover/row:flex flex-shrink-0">
454
+ <RowActionMenu card={card} isActionPending={isActionPending} onActionError={onActionError} onPendingActionChange={onPendingActionChange} readOnlyMode={readOnlyMode} />
455
+ </div>
342
456
  </div>
343
457
  {/* Subagent children */}
344
458
  {hasChildren && expanded && (
@@ -353,7 +467,7 @@ function SessionRow({ card, isLast, readOnly = false, isActionPending, onActionE
353
467
  <span className="text-gray-300 dark:text-zinc-600 text-xs flex-shrink-0 font-mono leading-none">
354
468
  {i === visibleChildren.length - 1 ? '└' : '├'}
355
469
  </span>
356
- <StatusDot status={child.realTimeStatus} waitingForUser={child.waitingForUser} />
470
+ <StatusDot status={child.realTimeStatus} waitingForUser={child.waitingForUser} provider={card.provider} />
357
471
  <span className="text-xs text-gray-500 dark:text-gray-400 truncate flex-1 min-w-0">
358
472
  {child.title || 'Subagent'}
359
473
  </span>
@@ -365,15 +479,36 @@ function SessionRow({ card, isLast, readOnly = false, isActionPending, onActionE
365
479
  );
366
480
  }
367
481
 
368
- export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, hostLabel: _hostLabel, multipleHostsEnabled }: ProjectCardProps) {
369
- const firstCard = cards[0];
370
- const readOnly = _readOnly ?? firstCard?.readOnly ?? false;
482
+ export function ProjectCard({ projectName, branch, cards, readOnly = false, hostLabel: _hostLabel, multipleHostsEnabled }: ProjectCardProps) {
483
+ const childSessionIds = useMemo(() => {
484
+ const ids = new Set<string>();
485
+ for (const card of cards) {
486
+ for (const child of card.children || []) {
487
+ ids.add(child.id);
488
+ }
489
+ }
490
+ return ids;
491
+ }, [cards]);
492
+
493
+ const deduplicatedCards = useMemo(() => {
494
+ return cards.filter((card) => {
495
+ if (!childSessionIds.has(card.id)) {
496
+ return true;
497
+ }
498
+
499
+ return (card.children?.length ?? 0) > 0;
500
+ });
501
+ }, [cards, childSessionIds]);
502
+
503
+ const firstCard = deduplicatedCards[0] ?? cards[0];
504
+ const actionableCards = deduplicatedCards;
505
+ const primaryCard = actionableCards[0] ?? firstCard;
371
506
  const hostLabel = _hostLabel ?? firstCard?.hostLabel;
372
- const hostId = firstCard?.hostId;
507
+ const hostId = primaryCard?.hostId ?? firstCard?.hostId;
373
508
  const showHostBadge = hostLabel && (multipleHostsEnabled || hostLabel !== 'Local');
374
509
  const hostAccentClass = getHostAccentTextClass(hostId, hostLabel);
375
510
  const [actionError, setActionError] = useState<string | null>(null);
376
- const [pendingAction, setPendingAction] = useState<'open' | 'archive' | 'delete' | null>(null);
511
+ const [pendingAction, setPendingAction] = useState<'open' | 'archive' | 'restore' | 'delete' | null>(null);
377
512
  const [openTool, setOpenTool] = useState(() => {
378
513
  if (typeof window === 'undefined') return 'vscode';
379
514
  return window.localStorage.getItem('vibepulse:open-tool') || 'vscode';
@@ -400,8 +535,8 @@ export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, h
400
535
  return storedHost;
401
536
  }
402
537
 
403
- const firstRemoteBaseUrl = firstCard?.hostBaseUrl;
404
- if (firstRemoteBaseUrl && firstCard?.hostId !== 'local') {
538
+ const firstRemoteBaseUrl = primaryCard?.hostBaseUrl;
539
+ if (firstRemoteBaseUrl && primaryCard?.hostId !== 'local') {
405
540
  try {
406
541
  return new URL(firstRemoteBaseUrl).hostname;
407
542
  } catch {
@@ -414,20 +549,27 @@ export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, h
414
549
  }
415
550
 
416
551
  return '';
417
- }, [firstCard?.hostBaseUrl, firstCard?.hostId]);
552
+ }, [primaryCard?.hostBaseUrl, primaryCard?.hostId]);
418
553
  const isActionPending = pendingAction !== null;
554
+ const isReadOnly = !!readOnly;
419
555
  const pendingActionLabel = pendingAction === 'open'
420
556
  ? 'Opening…'
421
557
  : pendingAction === 'archive'
422
558
  ? 'Archiving…'
423
- : pendingAction === 'delete'
424
- ? 'Deleting…'
425
- : null;
426
- const isRemoteProjectCard = !!firstCard && firstCard.hostId !== undefined && firstCard.hostId !== 'local';
559
+ : pendingAction === 'restore'
560
+ ? 'Restoring…'
561
+ : pendingAction === 'delete'
562
+ ? 'Deleting…'
563
+ : null;
564
+ const isRemoteProjectCard = !!primaryCard && primaryCard.hostId !== undefined && primaryCard.hostId !== 'local';
427
565
  const isConfigPendingForRemoteOpen = isRemoteProjectCard && isConfigLoading && !config;
428
566
  const isConfigUnavailableForRemoteOpen = isRemoteProjectCard && !config && !isConfigLoading;
429
567
 
430
568
  const handleOpenProject = () => {
569
+ if (isReadOnly) {
570
+ return;
571
+ }
572
+
431
573
  if (isActionPending) {
432
574
  return;
433
575
  }
@@ -441,24 +583,36 @@ export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, h
441
583
  return;
442
584
  }
443
585
 
444
- const directory = cards[0]?.directory;
586
+ const directory = primaryCard?.directory;
445
587
  if (!directory) return;
446
588
  setActionError(null);
447
- const firstProjectCard = cards[0];
589
+ const firstProjectCard = primaryCard;
448
590
  const openEditorTargetMode = config?.vibepulse?.openEditorTargetMode === 'hub' ? 'hub' : 'remote';
449
591
  const isRemoteProject = firstProjectCard?.hostId !== undefined && firstProjectCard.hostId !== 'local';
592
+ const canOpenEditor = firstProjectCard?.capabilities ? firstProjectCard.capabilities.openEditor : true;
450
593
 
451
594
  if (isRemoteProject && openEditorTargetMode === 'hub' && openTool === 'antigravity') {
452
595
  setActionError('Antigravity does not support hub-mode remote opens. Use VS Code or switch target mode to Remote node.');
453
596
  return;
454
597
  }
455
598
 
456
- const useRemoteSshTarget = isRemoteProject && openEditorTargetMode === 'hub' && openTool === 'vscode';
599
+ if (isRemoteProject && openEditorTargetMode === 'remote' && !canOpenEditor && openTool === 'antigravity') {
600
+ setActionError('Antigravity cannot open remote sessions without remote editor support. Use VS Code.');
601
+ return;
602
+ }
603
+
604
+ const useRemoteSshTarget =
605
+ isRemoteProject
606
+ && openTool === 'vscode'
607
+ && (
608
+ openEditorTargetMode === 'hub'
609
+ || (openEditorTargetMode === 'remote' && !canOpenEditor)
610
+ );
457
611
  const target = buildEditorUri(openTool === 'antigravity' ? 'antigravity' : 'vscode', directory, {
458
612
  remoteSshHost: useRemoteSshTarget ? remoteSshHost : null,
459
613
  });
460
614
 
461
- if (isRemoteProject && openEditorTargetMode === 'remote') {
615
+ if (isRemoteProject && openEditorTargetMode === 'remote' && canOpenEditor) {
462
616
  void (async () => {
463
617
  setPendingAction('open');
464
618
  try {
@@ -498,42 +652,40 @@ export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, h
498
652
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
499
653
  </svg>
500
654
  </div>
501
- <span className="text-sm font-semibold text-gray-800 dark:text-gray-200 truncate flex-1 min-w-0">
502
- {projectName}
503
- </span>
504
- {(cards.length > 1 || !readOnly) && (
505
- <div className="flex items-center flex-shrink-0 bg-gray-100 dark:bg-zinc-700/50 rounded-full h-6 border border-gray-200/50 dark:border-zinc-700">
506
- {cards.length > 1 && (
507
- <span className={`text-[11px] font-medium text-gray-500 dark:text-gray-400 ${!readOnly ? 'pl-2.5 pr-1' : 'px-2.5'}`}>
508
- {cards.length}
509
- </span>
510
- )}
511
- {!readOnly && (
512
- <div className={`${cards.length > 1 ? 'pr-0.5' : 'px-0.5'}`}>
513
- <HeaderActionMenu cards={cards} readOnly={readOnly} isActionPending={isActionPending} onActionError={setActionError} onPendingActionChange={setPendingAction} />
514
- </div>
515
- )}
655
+ <div className="flex-1 min-w-0 flex items-center gap-2">
656
+ <span className="text-sm font-semibold text-gray-800 dark:text-gray-200 truncate">
657
+ {projectName}
658
+ </span>
659
+ </div>
660
+ <div className="flex items-center flex-shrink-0 bg-gray-100 dark:bg-zinc-700/50 rounded-full h-6 border border-gray-200/50 dark:border-zinc-700">
661
+ {deduplicatedCards.length > 1 && (
662
+ <span className="text-[11px] font-medium text-gray-500 dark:text-gray-400 pl-2.5 pr-1">
663
+ {deduplicatedCards.length}
664
+ </span>
665
+ )}
666
+ <div className={`${deduplicatedCards.length > 1 ? 'pr-0.5' : 'px-0.5'}`}>
667
+ <HeaderActionMenu cards={actionableCards} isActionPending={isActionPending} onActionError={setActionError} onPendingActionChange={setPendingAction} readOnlyMode={isReadOnly} />
516
668
  </div>
517
- )}
669
+ </div>
518
670
  </div>
519
671
 
520
672
  {/* Session rows */}
521
673
  <div className="border-t border-gray-100 dark:border-zinc-700/50">
522
- {cards.map((card, index) => (
674
+ {deduplicatedCards.map((card, index) => (
523
675
  <SessionRow
524
676
  key={card.id}
525
677
  card={card}
526
- isLast={index === cards.length - 1}
527
- readOnly={readOnly}
678
+ isLast={index === deduplicatedCards.length - 1}
528
679
  isActionPending={isActionPending}
529
680
  onActionError={setActionError}
530
681
  onPendingActionChange={setPendingAction}
682
+ readOnlyMode={isReadOnly}
531
683
  />
532
684
  ))}
533
685
  </div>
534
686
 
535
687
  {/* Footer */}
536
- {!readOnly && (
688
+ {(branch || primaryCard?.directory) && (
537
689
  <div className="flex items-center justify-between gap-2 px-3 py-1.5 border-t border-gray-100 dark:border-zinc-700/50 bg-gray-50/50 dark:bg-zinc-800/50">
538
690
  <div className="min-w-0 flex-1 text-[10px] text-gray-400 dark:text-gray-500 truncate">
539
691
  {branch ? (
@@ -550,7 +702,7 @@ export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, h
550
702
  window.localStorage.setItem('vibepulse:open-tool', e.target.value);
551
703
  }}
552
704
  title="Select open tool"
553
- disabled={isActionPending || isConfigPendingForRemoteOpen || isConfigUnavailableForRemoteOpen}
705
+ disabled={isReadOnly || isActionPending || isConfigPendingForRemoteOpen || isConfigUnavailableForRemoteOpen}
554
706
  >
555
707
  <option value="vscode">VSCode</option>
556
708
  <option value="antigravity">Antigravity</option>
@@ -560,7 +712,7 @@ export function ProjectCard({ projectName, branch, cards, readOnly: _readOnly, h
560
712
  onClick={handleOpenProject}
561
713
  className="flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium text-gray-500 hover:text-blue-600 hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-400 dark:hover:text-blue-400 dark:hover:bg-blue-900/20 transition-colors"
562
714
  title="Open project"
563
- disabled={isActionPending || isConfigPendingForRemoteOpen || isConfigUnavailableForRemoteOpen}
715
+ disabled={isReadOnly || isActionPending || isConfigPendingForRemoteOpen || isConfigUnavailableForRemoteOpen}
564
716
  >
565
717
  <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
566
718
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />