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
@@ -196,7 +196,7 @@ describe('SessionCard', () => {
196
196
  expect((fetchMock.mock.calls as unknown as Array<[RequestInfo | URL, RequestInit | undefined]>).filter(([, init]) => init?.method === 'POST')).toHaveLength(0);
197
197
  });
198
198
 
199
- it('calls the hub open-editor route for remote mode remote sessions', async () => {
199
+ it('calls the hub open-editor route for remote mode sessions that support openEditor', async () => {
200
200
  const queryClient = new QueryClient({
201
201
  defaultOptions: {
202
202
  queries: { retry: false },
@@ -213,7 +213,14 @@ describe('SessionCard', () => {
213
213
 
214
214
  render(
215
215
  <QueryClientProvider client={queryClient}>
216
- <SessionCard card={createCard()} />
216
+ <SessionCard card={createCard({
217
+ capabilities: {
218
+ openProject: true,
219
+ openEditor: true,
220
+ archive: true,
221
+ delete: true,
222
+ },
223
+ })} />
217
224
  </QueryClientProvider>
218
225
  );
219
226
 
@@ -234,6 +241,84 @@ describe('SessionCard', () => {
234
241
  expect(window.location.assign).not.toHaveBeenCalled();
235
242
  });
236
243
 
244
+ it('falls back to file open when remote mode is selected but openEditor capability is unsupported', async () => {
245
+ const queryClient = new QueryClient({
246
+ defaultOptions: {
247
+ queries: { retry: false },
248
+ },
249
+ });
250
+ queryClient.setQueryData(['opencode-config'], { vibepulse: { openEditorTargetMode: 'remote' } });
251
+ const fetchMock = vi.fn(async () => new Response(JSON.stringify({ success: true }), {
252
+ status: 200,
253
+ headers: { 'Content-Type': 'application/json' },
254
+ }));
255
+ Object.defineProperty(globalThis, 'fetch', { value: fetchMock, configurable: true });
256
+
257
+ render(
258
+ <QueryClientProvider client={queryClient}>
259
+ <SessionCard card={createCard({
260
+ provider: 'claude-code',
261
+ readOnly: true,
262
+ capabilities: {
263
+ openProject: true,
264
+ openEditor: false,
265
+ archive: false,
266
+ delete: false,
267
+ },
268
+ })} />
269
+ </QueryClientProvider>
270
+ );
271
+
272
+ await screen.findByText('Remote Session');
273
+ await waitFor(() => {
274
+ expect(screen.getByRole('button', { name: /remote session/i })).not.toBeDisabled();
275
+ });
276
+ fireEvent.doubleClick(screen.getByRole('button', { name: /remote session/i }));
277
+
278
+ expect(window.location.assign).toHaveBeenCalledWith('vscode://vscode-remote/ssh-remote+node-1.test/tmp/demo');
279
+ expect((fetchMock.mock.calls as unknown as Array<[RequestInfo | URL, RequestInit | undefined]>).filter(([, init]) => init?.method === 'POST')).toHaveLength(0);
280
+ });
281
+
282
+ it('shows actionable error when Antigravity is selected for remote fallback without openEditor support', async () => {
283
+ window.localStorage.setItem('vibepulse:open-tool', 'antigravity');
284
+ const queryClient = new QueryClient({
285
+ defaultOptions: {
286
+ queries: { retry: false },
287
+ },
288
+ });
289
+ queryClient.setQueryData(['opencode-config'], { vibepulse: { openEditorTargetMode: 'remote' } });
290
+ const fetchMock = vi.fn(async () => new Response(JSON.stringify({ success: true }), {
291
+ status: 200,
292
+ headers: { 'Content-Type': 'application/json' },
293
+ }));
294
+ Object.defineProperty(globalThis, 'fetch', { value: fetchMock, configurable: true });
295
+
296
+ render(
297
+ <QueryClientProvider client={queryClient}>
298
+ <SessionCard card={createCard({
299
+ provider: 'claude-code',
300
+ readOnly: true,
301
+ capabilities: {
302
+ openProject: true,
303
+ openEditor: false,
304
+ archive: false,
305
+ delete: false,
306
+ },
307
+ })} />
308
+ </QueryClientProvider>
309
+ );
310
+
311
+ await screen.findByText('Remote Session');
312
+ await waitFor(() => {
313
+ expect(screen.getByRole('button', { name: /remote session/i })).not.toBeDisabled();
314
+ });
315
+ fireEvent.doubleClick(screen.getByRole('button', { name: /remote session/i }));
316
+
317
+ expect(await screen.findByText('Antigravity cannot open remote sessions without remote editor support. Use VS Code.')).toBeTruthy();
318
+ expect(window.location.assign).not.toHaveBeenCalled();
319
+ expect((fetchMock.mock.calls as unknown as Array<[RequestInfo | URL, RequestInit | undefined]>).filter(([, init]) => init?.method === 'POST')).toHaveLength(0);
320
+ });
321
+
237
322
  it('shows an explicit loading state while a remote open request is in flight', async () => {
238
323
  const queryClient = new QueryClient({
239
324
  defaultOptions: {
@@ -515,6 +600,91 @@ describe('SessionCard', () => {
515
600
  expect(await screen.findByText('Session was not found.')).toBeTruthy();
516
601
  });
517
602
 
603
+ it('respects capabilities for action visibility over readOnly alone', async () => {
604
+ const capabilityCard = createCard({
605
+ readOnly: true,
606
+ capabilities: {
607
+ openProject: true,
608
+ openEditor: true,
609
+ archive: true,
610
+ delete: false,
611
+ },
612
+ });
613
+
614
+ const user = userEvent.setup();
615
+ renderWithProviders(<SessionCard card={capabilityCard} />);
616
+
617
+ await screen.findByText('Remote Session');
618
+
619
+ await user.click(screen.getByTitle('Actions'));
620
+
621
+ expect(screen.getByRole('button', { name: 'Archive' })).toBeTruthy();
622
+ expect(screen.queryByRole('button', { name: 'Delete' })).toBeNull();
623
+ });
624
+
625
+ it('shows no actions menu if both archive and delete capabilities are false', async () => {
626
+ const capabilityCard = createCard({
627
+ capabilities: {
628
+ openProject: true,
629
+ openEditor: true,
630
+ archive: false,
631
+ delete: false,
632
+ },
633
+ });
634
+
635
+ renderWithProviders(<SessionCard card={capabilityCard} />);
636
+
637
+ await screen.findByText('Remote Session');
638
+
639
+ expect(screen.queryByTitle('Actions')).toBeNull();
640
+ });
641
+
642
+ it('shows archive and delete actions for Claude sessions when capabilities allow them', async () => {
643
+ renderWithProviders(<SessionCard card={createCard({
644
+ provider: 'claude-code',
645
+ readOnly: true,
646
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
647
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
648
+ capabilities: {
649
+ openProject: true,
650
+ openEditor: false,
651
+ archive: true,
652
+ delete: true,
653
+ },
654
+ })} />);
655
+
656
+ expect(screen.getByTitle('Actions')).toBeTruthy();
657
+ });
658
+
659
+ it('shows restore for archived Claude sessions', async () => {
660
+ const fetchMock = vi.fn(async () => new Response(JSON.stringify({ success: true }), {
661
+ status: 200,
662
+ headers: { 'Content-Type': 'application/json' },
663
+ }));
664
+ Object.defineProperty(globalThis, 'fetch', { value: fetchMock, configurable: true });
665
+
666
+ renderWithProviders(<SessionCard card={createCard({
667
+ provider: 'claude-code',
668
+ readOnly: true,
669
+ status: 'done',
670
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
671
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
672
+ capabilities: {
673
+ openProject: true,
674
+ openEditor: false,
675
+ archive: true,
676
+ delete: true,
677
+ },
678
+ })} />);
679
+
680
+ fireEvent.click(screen.getByTitle('Actions'));
681
+ fireEvent.click(screen.getByText('Restore'));
682
+
683
+ await waitFor(() => {
684
+ expect(fetchMock).toHaveBeenCalledWith('/api/sessions/local:claude~550e8400-e29b-41d4-a716-446655440000/restore', expect.objectContaining({ method: 'POST' }));
685
+ });
686
+ });
687
+
518
688
  it('shows a loading-settings state before remote open mode is hydrated', async () => {
519
689
  const fetchMock = vi.fn(async () => new Promise<Response>(() => {}));
520
690
  Object.defineProperty(globalThis, 'fetch', { value: fetchMock, configurable: true });
@@ -563,4 +733,85 @@ describe('SessionCard', () => {
563
733
 
564
734
  expect(await screen.findByText('Remote node is offline or unreachable.')).toBeTruthy();
565
735
  });
736
+
737
+ it('shows recently-updated idle children so delegated subagents remain visible', () => {
738
+ const now = Date.now();
739
+ renderWithProviders(
740
+ <SessionCard
741
+ card={createCard({
742
+ hostId: 'local',
743
+ hostLabel: 'Local',
744
+ hostKind: 'local',
745
+ hostBaseUrl: undefined,
746
+ children: [
747
+ {
748
+ id: 'child-recent-idle',
749
+ title: 'Recent Idle Child',
750
+ realTimeStatus: 'idle',
751
+ waitingForUser: false,
752
+ createdAt: now - 120_000,
753
+ updatedAt: now - 20_000,
754
+ },
755
+ ],
756
+ })}
757
+ />
758
+ );
759
+
760
+ expect(screen.getByText('Recent Idle Child')).toBeTruthy();
761
+ });
762
+
763
+ it('hides stale idle children once they are older than the recent visibility window', () => {
764
+ const now = Date.now();
765
+ renderWithProviders(
766
+ <SessionCard
767
+ card={createCard({
768
+ hostId: 'local',
769
+ hostLabel: 'Local',
770
+ hostKind: 'local',
771
+ hostBaseUrl: undefined,
772
+ children: [
773
+ {
774
+ id: 'child-stale-idle',
775
+ title: 'Stale Idle Child',
776
+ realTimeStatus: 'idle',
777
+ waitingForUser: false,
778
+ createdAt: now - 180_000,
779
+ updatedAt: now - 120_000,
780
+ },
781
+ ],
782
+ })}
783
+ />
784
+ );
785
+
786
+ expect(screen.queryByText('Stale Idle Child')).toBeNull();
787
+ });
788
+ });
789
+ describe('SessionCard Provider Visuals', () => {
790
+ it('does not show a provider marker for OpenCode sessions', () => {
791
+ renderWithProviders(<SessionCard card={createCard({ provider: 'opencode' })} />);
792
+ expect(screen.queryByTitle('Provider: OpenCode')).toBeNull();
793
+ });
794
+
795
+ it('renders Claude status as a diamond instead of showing a separate provider marker', () => {
796
+ renderWithProviders(<SessionCard card={createCard({ provider: 'claude-code' })} />);
797
+ expect(screen.getByTitle('Idle').className).toContain('rotate-45');
798
+ expect(screen.getByTitle('Idle').className).toContain('h-[9px]');
799
+ });
800
+
801
+ it('does not render transcript affordances for Claude cards even when transcript metadata exists', () => {
802
+ const claudeCardWithTranscript: KanbanCard & { hasTranscript: boolean } = {
803
+ ...createCard({
804
+ provider: 'claude-code',
805
+ readOnly: true,
806
+ rawSessionId: '550e8400-e29b-41d4-a716-446655440000',
807
+ id: 'local:claude~550e8400-e29b-41d4-a716-446655440000',
808
+ }),
809
+ hasTranscript: true,
810
+ };
811
+
812
+ renderWithProviders(<SessionCard card={claudeCardWithTranscript} />);
813
+
814
+ expect(screen.queryByRole('button', { name: /transcript/i })).toBeNull();
815
+ expect(screen.queryByText(/transcript/i)).toBeNull();
816
+ });
566
817
  });
@@ -17,6 +17,16 @@ interface SessionCardProps {
17
17
  card: KanbanCard;
18
18
  }
19
19
 
20
+ const RECENT_IDLE_CHILD_VISIBILITY_WINDOW_MS = 60_000;
21
+
22
+ function shouldShowChildSession(child: { realTimeStatus: string; waitingForUser: boolean; updatedAt: number }): boolean {
23
+ if (child.realTimeStatus !== 'idle' || child.waitingForUser) {
24
+ return true;
25
+ }
26
+
27
+ return Date.now() - child.updatedAt <= RECENT_IDLE_CHILD_VISIBILITY_WINDOW_MS;
28
+ }
29
+
20
30
  function formatRelativeTime(timestamp: number): string {
21
31
  const diffMs = Date.now() - timestamp;
22
32
  const diffMins = Math.floor(diffMs / (1000 * 60));
@@ -30,13 +40,16 @@ function formatRelativeTime(timestamp: number): string {
30
40
  }
31
41
 
32
42
  // Status indicator component
33
- function StatusIndicator({ status, waitingForUser }: { status: string; waitingForUser: boolean }) {
43
+ function StatusIndicator({ status, waitingForUser, provider }: { status: string; waitingForUser: boolean; provider?: string }) {
44
+ const isClaudeProvider = provider === 'claude-code';
45
+ const shapeClass = isClaudeProvider ? 'rotate-45 rounded-[1px]' : 'rounded-full';
46
+ const dotSizeClass = isClaudeProvider ? 'h-[9px] w-[9px]' : 'h-2.5 w-2.5';
34
47
  if (waitingForUser) {
35
48
  return (
36
49
  <div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
37
- <span className="relative flex h-2.5 w-2.5">
38
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
39
- <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-amber-500"></span>
50
+ <span className={`relative flex ${dotSizeClass}`}>
51
+ <span className={`animate-ping absolute inline-flex h-full w-full ${shapeClass} bg-amber-400 opacity-75`} title="Waiting"></span>
52
+ <span className={`relative inline-flex ${dotSizeClass} ${shapeClass} bg-amber-500`}></span>
40
53
  </span>
41
54
  <span className="text-xs font-medium">Waiting</span>
42
55
  </div>
@@ -46,9 +59,9 @@ function StatusIndicator({ status, waitingForUser }: { status: string; waitingFo
46
59
  case 'busy':
47
60
  return (
48
61
  <div className="flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
49
- <span className="relative flex h-2.5 w-2.5">
50
- <span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
51
- <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
62
+ <span className={`relative flex ${dotSizeClass}`}>
63
+ <span className={`animate-pulse absolute inline-flex h-full w-full ${shapeClass} bg-emerald-400 opacity-75`} title="Running"></span>
64
+ <span className={`relative inline-flex ${dotSizeClass} ${shapeClass} bg-emerald-500`}></span>
52
65
  </span>
53
66
  <span className="text-xs font-medium">Running</span>
54
67
  </div>
@@ -56,9 +69,9 @@ function StatusIndicator({ status, waitingForUser }: { status: string; waitingFo
56
69
  case 'retry':
57
70
  return (
58
71
  <div className="flex items-center gap-1.5 text-red-600 dark:text-red-400">
59
- <span className="relative flex h-2.5 w-2.5">
60
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
61
- <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-red-500"></span>
72
+ <span className={`relative flex ${dotSizeClass}`}>
73
+ <span className={`animate-ping absolute inline-flex h-full w-full ${shapeClass} bg-red-400 opacity-75`} title="Retrying"></span>
74
+ <span className={`relative inline-flex ${dotSizeClass} ${shapeClass} bg-red-500`}></span>
62
75
  </span>
63
76
  <span className="text-xs font-medium">Retrying</span>
64
77
  </div>
@@ -67,7 +80,7 @@ function StatusIndicator({ status, waitingForUser }: { status: string; waitingFo
67
80
  default:
68
81
  return (
69
82
  <div className="flex items-center gap-1.5 text-gray-500 dark:text-gray-400">
70
- <span className="inline-flex rounded-full h-2.5 w-2.5 bg-gray-400"></span>
83
+ <span className={`inline-flex ${dotSizeClass} bg-gray-400 ${shapeClass}`} title="Idle"></span>
71
84
  <span className="text-xs font-medium">Idle</span>
72
85
  </div>
73
86
  );
@@ -80,7 +93,7 @@ export function SessionCard({ card }: SessionCardProps) {
80
93
  const [remoteSshHost, setRemoteSshHost] = useState('');
81
94
  const [actionOpen, setActionOpen] = useState(false);
82
95
  const [actionError, setActionError] = useState<string | null>(null);
83
- const [pendingAction, setPendingAction] = useState<'open' | 'archive' | 'delete' | null>(null);
96
+ const [pendingAction, setPendingAction] = useState<'open' | 'archive' | 'restore' | 'delete' | null>(null);
84
97
  const actionMenuRef = useRef<HTMLDivElement>(null);
85
98
  const { data: config, isLoading: isConfigLoading, isError: isConfigError } = useQuery<ConfigResponse>({
86
99
  queryKey: ['opencode-config'],
@@ -108,9 +121,11 @@ export function SessionCard({ card }: SessionCardProps) {
108
121
  ? 'Opening…'
109
122
  : pendingAction === 'archive'
110
123
  ? 'Archiving…'
111
- : pendingAction === 'delete'
112
- ? 'Deleting…'
113
- : null;
124
+ : pendingAction === 'restore'
125
+ ? 'Restoring…'
126
+ : pendingAction === 'delete'
127
+ ? 'Deleting…'
128
+ : null;
114
129
 
115
130
  useEffect(() => {
116
131
  const storedTool = window.localStorage.getItem('vibepulse:open-tool');
@@ -181,12 +196,24 @@ export function SessionCard({ card }: SessionCardProps) {
181
196
  return;
182
197
  }
183
198
 
184
- const useRemoteSshTarget = isRemoteCard && openEditorTargetMode === 'hub' && openTool === 'vscode';
199
+ const canOpenEditor = card.capabilities ? card.capabilities.openEditor : true;
200
+ if (isRemoteCard && openEditorTargetMode === 'remote' && !canOpenEditor && openTool === 'antigravity') {
201
+ setActionError('Antigravity cannot open remote sessions without remote editor support. Use VS Code.');
202
+ return;
203
+ }
204
+
205
+ const useRemoteSshTarget =
206
+ isRemoteCard
207
+ && openTool === 'vscode'
208
+ && (
209
+ openEditorTargetMode === 'hub'
210
+ || (openEditorTargetMode === 'remote' && !canOpenEditor)
211
+ );
185
212
  const target = buildEditorUri(openTool === 'antigravity' ? 'antigravity' : 'vscode', card.directory, {
186
213
  remoteSshHost: useRemoteSshTarget ? remoteSshHost : null,
187
214
  });
188
215
 
189
- if (isRemoteCard && openEditorTargetMode === 'remote') {
216
+ if (isRemoteCard && openEditorTargetMode === 'remote' && canOpenEditor) {
190
217
  setPendingAction('open');
191
218
  try {
192
219
  const response = await fetch(`/api/sessions/${card.id}/open-editor`, {
@@ -216,6 +243,9 @@ export function SessionCard({ card }: SessionCardProps) {
216
243
  return;
217
244
  }
218
245
 
246
+ const canArchive = card.capabilities ? card.capabilities.archive : !card.readOnly;
247
+ if (!canArchive) return;
248
+
219
249
  setActionError(null);
220
250
  setPendingAction('archive');
221
251
  try {
@@ -233,11 +263,39 @@ export function SessionCard({ card }: SessionCardProps) {
233
263
  }
234
264
  };
235
265
 
266
+ const handleRestore = async () => {
267
+ if (isActionPending) {
268
+ return;
269
+ }
270
+
271
+ const canArchive = card.capabilities ? card.capabilities.archive : !card.readOnly;
272
+ if (!canArchive) return;
273
+
274
+ setActionError(null);
275
+ setPendingAction('restore');
276
+ try {
277
+ const response = await fetch(`/api/sessions/${card.id}/restore`, { method: 'POST' });
278
+ if (!response.ok) {
279
+ const errorBody = await response.json().catch(() => null);
280
+ setActionError(mapSessionActionError(errorBody, 'Failed to restore session'));
281
+ }
282
+ } catch {
283
+ setActionError('Remote node is offline or unreachable.');
284
+ } finally {
285
+ setPendingAction(null);
286
+ setActionOpen(false);
287
+ await queryClient.invalidateQueries({ queryKey: ['sessions'] });
288
+ }
289
+ };
290
+
236
291
  const handleDelete = async () => {
237
292
  if (isActionPending) {
238
293
  return;
239
294
  }
240
295
 
296
+ const canDelete = card.capabilities ? card.capabilities.delete : !card.readOnly;
297
+ if (!canDelete) return;
298
+
241
299
  setActionError(null);
242
300
  setPendingAction('delete');
243
301
  try {
@@ -255,6 +313,10 @@ export function SessionCard({ card }: SessionCardProps) {
255
313
  }
256
314
  };
257
315
 
316
+ const canArchive = card.capabilities ? card.capabilities.archive : !card.readOnly;
317
+ const canDelete = card.capabilities ? card.capabilities.delete : !card.readOnly;
318
+ const showActionsMenu = canArchive || canDelete;
319
+
258
320
  return (
259
321
  <article
260
322
  className="relative w-full text-left p-4 bg-white dark:bg-zinc-800 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-700 hover:shadow-lg hover:border-gray-300 dark:hover:border-zinc-600 transition-all duration-200"
@@ -269,7 +331,7 @@ export function SessionCard({ card }: SessionCardProps) {
269
331
  >
270
332
  {/* Top: Status indicator */}
271
333
  <div className="flex items-center justify-between mb-2">
272
- <StatusIndicator status={card.opencodeStatus} waitingForUser={card.waitingForUser} />
334
+ <StatusIndicator status={card.opencodeStatus} waitingForUser={card.waitingForUser} provider={card.provider} />
273
335
  </div>
274
336
  <h3
275
337
  className="font-semibold text-gray-900 dark:text-gray-100 text-base line-clamp-2"
@@ -278,18 +340,17 @@ export function SessionCard({ card }: SessionCardProps) {
278
340
  {card.title || 'Untitled Session'}
279
341
  </h3>
280
342
 
281
- {card.agents.length > 0 && (
282
- <div className="flex flex-wrap gap-1 mt-2">
283
- {card.agents.map((agent) => (
284
- <span
285
- key={agent}
286
- className="px-2 py-0.5 bg-indigo-50 text-indigo-700 text-sm rounded-full font-medium"
287
- >
288
- {agent}
289
- </span>
290
- ))}
291
- </div>
292
- )}
343
+ <div className="flex flex-wrap gap-1 mt-2">
344
+ {card.agents.map((agent) => (
345
+ <span
346
+ key={agent}
347
+ className="px-2 py-0.5 bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400 text-xs rounded-full font-medium"
348
+ >
349
+ {agent}
350
+ </span>
351
+ ))}
352
+ </div>
353
+
293
354
  {card.projectName && (
294
355
  <div className="mt-2 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
295
356
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" role="img" aria-hidden="true">
@@ -338,7 +399,33 @@ export function SessionCard({ card }: SessionCardProps) {
338
399
  </div>
339
400
  </div>
340
401
  )}
402
+ {(() => {
403
+ const visibleChildren = (card.children || []).filter(
404
+ shouldShowChildSession
405
+ );
406
+ if (visibleChildren.length === 0) return null;
407
+ return (
408
+ <div className="mt-3 bg-gray-50/50 dark:bg-zinc-900/30 -mx-4 px-4 py-2 border-y border-gray-100 dark:border-zinc-700/50">
409
+ {visibleChildren.map((child, i) => (
410
+ <div
411
+ key={child.id}
412
+ className="flex items-center gap-2 pl-2 pr-3 py-1.5"
413
+ title={child.debugReason ? `Reason: ${child.debugReason}` : 'Subagent'}
414
+ >
415
+ <span className="text-gray-300 dark:text-zinc-600 text-xs flex-shrink-0 font-mono leading-none">
416
+ {i === visibleChildren.length - 1 ? '└' : '├'}
417
+ </span>
418
+ <StatusIndicator status={child.realTimeStatus} waitingForUser={child.waitingForUser} provider={card.provider} />
419
+ <span className="text-xs text-gray-500 dark:text-gray-400 truncate flex-1 min-w-0">
420
+ {child.title || 'Subagent'}
421
+ </span>
422
+ </div>
423
+ ))}
424
+ </div>
425
+ );
426
+ })()}
341
427
  {isConfigPendingForRemoteOpen ? (
428
+
342
429
  <div className="mt-3 rounded-md border border-blue-200 bg-blue-50 px-2 py-1 text-xs text-blue-700 dark:border-blue-900/40 dark:bg-blue-900/20 dark:text-blue-300">
343
430
  Loading open settings…
344
431
  </div>
@@ -382,52 +469,71 @@ export function SessionCard({ card }: SessionCardProps) {
382
469
  SSH: {remoteSshHost}
383
470
  </span>
384
471
  )}
385
- <div className="relative" ref={actionMenuRef}>
386
- <button
387
- type="button"
388
- className="inline-flex items-center justify-center w-6 h-6 rounded-md text-gray-400 hover:text-gray-700 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-200 dark:hover:bg-zinc-700"
389
- onClick={(e) => {
390
- e.stopPropagation();
391
- if (!isActionPending) {
392
- setActionOpen((prev) => !prev);
393
- }
394
- }}
395
- onDoubleClick={(e) => e.stopPropagation()}
396
- aria-label="Actions"
397
- title="Actions"
398
- disabled={isActionPending}
399
- >
400
- <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" role="img" aria-hidden="true">
401
- <path d="M5 10a2 2 0 110 4 2 2 0 010-4zm7 0a2 2 0 110 4 2 2 0 010-4zm7 0a2 2 0 110 4 2 2 0 010-4z" />
402
- </svg>
403
- </button>
404
- {actionOpen && (
405
- <div className="absolute right-0 mt-1 w-36 rounded-md border border-gray-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900 z-10">
406
- <button
407
- type="button"
408
- className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-200 dark:hover:bg-zinc-800"
409
- onClick={(e) => {
410
- e.stopPropagation();
411
- handleArchive();
412
- }}
413
- disabled={isActionPending}
414
- >
415
- Archive
416
- </button>
417
- <button
418
- type="button"
419
- className="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-400 dark:hover:bg-red-900/20"
420
- onClick={(e) => {
421
- e.stopPropagation();
422
- handleDelete();
423
- }}
424
- disabled={isActionPending}
425
- >
426
- Delete
427
- </button>
428
- </div>
429
- )}
430
- </div>
472
+ {showActionsMenu && (
473
+ <div className="relative" ref={actionMenuRef}>
474
+ <button
475
+ type="button"
476
+ className="inline-flex items-center justify-center w-6 h-6 rounded-md text-gray-400 hover:text-gray-700 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-200 dark:hover:bg-zinc-700"
477
+ onClick={(e) => {
478
+ e.stopPropagation();
479
+ if (!isActionPending) {
480
+ setActionOpen((prev) => !prev);
481
+ }
482
+ }}
483
+ onDoubleClick={(e) => e.stopPropagation()}
484
+ aria-label="Actions"
485
+ title="Actions"
486
+ disabled={isActionPending}
487
+ >
488
+ <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" role="img" aria-hidden="true">
489
+ <path d="M5 10a2 2 0 110 4 2 2 0 010-4zm7 0a2 2 0 110 4 2 2 0 010-4zm7 0a2 2 0 110 4 2 2 0 010-4z" />
490
+ </svg>
491
+ </button>
492
+ {actionOpen && (
493
+ <div className="absolute right-0 mt-1 w-36 rounded-md border border-gray-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900 z-10">
494
+ {(card.status !== 'done' && canArchive) && (
495
+ <button
496
+ type="button"
497
+ className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-200 dark:hover:bg-zinc-800"
498
+ onClick={(e) => {
499
+ e.stopPropagation();
500
+ handleArchive();
501
+ }}
502
+ disabled={isActionPending}
503
+ >
504
+ Archive
505
+ </button>
506
+ )}
507
+ {(card.status === 'done' && canArchive) && (
508
+ <button
509
+ type="button"
510
+ className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-200 dark:hover:bg-zinc-800"
511
+ onClick={(e) => {
512
+ e.stopPropagation();
513
+ handleRestore();
514
+ }}
515
+ disabled={isActionPending}
516
+ >
517
+ Restore
518
+ </button>
519
+ )}
520
+ {canDelete && (
521
+ <button
522
+ type="button"
523
+ className="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-400 dark:hover:bg-red-900/20"
524
+ onClick={(e) => {
525
+ e.stopPropagation();
526
+ handleDelete();
527
+ }}
528
+ disabled={isActionPending}
529
+ >
530
+ Delete
531
+ </button>
532
+ )}
533
+ </div>
534
+ )}
535
+ </div>
536
+ )}
431
537
  </div>
432
538
  </article>
433
539
  );