@shepai/cli 1.53.2 → 1.54.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 (262) hide show
  1. package/apis/json-schema/NotificationEvent.yaml +4 -0
  2. package/dist/packages/core/src/domain/generated/output.d.ts +4 -0
  3. package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
  4. package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts +4 -0
  5. package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts.map +1 -1
  6. package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.js +19 -6
  7. package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts +5 -3
  8. package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts.map +1 -1
  9. package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.js +21 -13
  10. package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
  11. package/dist/src/presentation/cli/commands/ui.command.js +2 -1
  12. package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts +13 -0
  13. package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts.map +1 -0
  14. package/dist/src/presentation/web/app/api/agent-events/health/route.js +60 -0
  15. package/dist/src/presentation/web/app/api/agent-events/route.d.ts +8 -4
  16. package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -1
  17. package/dist/src/presentation/web/app/api/agent-events/route.js +227 -55
  18. package/dist/src/presentation/web/app/layout.d.ts.map +1 -1
  19. package/dist/src/presentation/web/app/layout.js +14 -0
  20. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts +5 -1
  21. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  22. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +29 -1
  23. package/dist/src/presentation/web/components/common/repository-node/repository-node.d.ts.map +1 -1
  24. package/dist/src/presentation/web/components/common/repository-node/repository-node.js +1 -1
  25. package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts +2 -0
  26. package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts.map +1 -0
  27. package/dist/src/presentation/web/components/common/sound-toggle/index.js +1 -0
  28. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts +2 -0
  29. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts.map +1 -0
  30. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.js +9 -0
  31. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts +8 -0
  32. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts.map +1 -0
  33. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.js +17 -0
  34. package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
  35. package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +14 -2
  36. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +1 -0
  37. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
  38. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +151 -19
  39. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.d.ts.map +1 -1
  40. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +2 -1
  41. package/dist/src/presentation/web/dev-server.js +14 -0
  42. package/dist/src/presentation/web/hooks/use-agent-events.d.ts +7 -0
  43. package/dist/src/presentation/web/hooks/use-agent-events.d.ts.map +1 -1
  44. package/dist/src/presentation/web/hooks/use-agent-events.js +135 -47
  45. package/dist/src/presentation/web/hooks/use-notifications.d.ts.map +1 -1
  46. package/dist/src/presentation/web/hooks/use-notifications.js +56 -16
  47. package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts.map +1 -1
  48. package/dist/src/presentation/web/hooks/use-notifications.stories.js +28 -9
  49. package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts +6 -0
  50. package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts.map +1 -0
  51. package/dist/src/presentation/web/hooks/use-sound-enabled.js +27 -0
  52. package/dist/src/presentation/web/hooks/use-sound.d.ts +15 -0
  53. package/dist/src/presentation/web/hooks/use-sound.d.ts.map +1 -0
  54. package/dist/src/presentation/web/hooks/use-sound.js +83 -0
  55. package/dist/src/presentation/web/hooks/use-sound.stories.d.ts +8 -0
  56. package/dist/src/presentation/web/hooks/use-sound.stories.d.ts.map +1 -0
  57. package/dist/src/presentation/web/hooks/use-sound.stories.js +67 -0
  58. package/dist/src/presentation/web/public/agent-events-sw.d.ts +35 -0
  59. package/dist/src/presentation/web/public/agent-events-sw.d.ts.map +1 -0
  60. package/dist/src/presentation/web/public/agent-events-sw.js +140 -0
  61. package/dist/tsconfig.build.tsbuildinfo +1 -1
  62. package/package.json +1 -1
  63. package/web/.next/BUILD_ID +1 -1
  64. package/web/.next/app-path-routes-manifest.json +1 -0
  65. package/web/.next/build-manifest.json +2 -2
  66. package/web/.next/cache/.previewinfo +1 -1
  67. package/web/.next/cache/.rscinfo +1 -1
  68. package/web/.next/cache/.tsbuildinfo +1 -1
  69. package/web/.next/cache/config.json +3 -3
  70. package/web/.next/fallback-build-manifest.json +2 -2
  71. package/web/.next/prerender-manifest.json +3 -3
  72. package/web/.next/required-server-files.js +1 -1
  73. package/web/.next/required-server-files.json +1 -1
  74. package/web/.next/routes-manifest.json +6 -0
  75. package/web/.next/server/app/_global-error.html +2 -2
  76. package/web/.next/server/app/_global-error.rsc +1 -1
  77. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  78. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  79. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  80. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  81. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  82. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  83. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  84. package/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
  85. package/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
  86. package/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
  87. package/web/.next/server/app/api/agent-events/health/route.js +6 -0
  88. package/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
  89. package/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
  90. package/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
  91. package/web/.next/server/app/api/agent-events/route.js +2 -1
  92. package/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
  93. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  94. package/web/.next/server/app/page.js.nft.json +1 -1
  95. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  96. package/web/.next/server/app/version/page.js +1 -1
  97. package/web/.next/server/app/version/page.js.nft.json +1 -1
  98. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  99. package/web/.next/server/app-paths-manifest.json +1 -0
  100. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
  101. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -0
  102. package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js +3 -0
  103. package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js.map +1 -0
  104. package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
  105. package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js.map +1 -0
  106. package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
  107. package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js.map +1 -0
  108. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  109. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  110. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  111. package/web/.next/{standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js → server/chunks/ssr/[root-of-the-server]__6e8b5181._.js} +2 -2
  112. package/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
  113. package/web/.next/server/chunks/ssr/_1632a21f._.js.map +1 -0
  114. package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
  115. package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js.map +1 -1
  116. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  117. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  118. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  119. package/web/.next/server/pages/500.html +2 -2
  120. package/web/.next/server/server-reference-manifest.js +1 -1
  121. package/web/.next/server/server-reference-manifest.json +15 -15
  122. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  123. package/web/.next/standalone/src/presentation/web/.next/app-path-routes-manifest.json +1 -0
  124. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  125. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  126. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  127. package/web/.next/standalone/src/presentation/web/.next/routes-manifest.json +6 -0
  128. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  129. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  130. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  131. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  132. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  133. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  134. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  135. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  136. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  137. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
  138. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
  139. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
  140. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js +6 -0
  141. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
  142. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
  143. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
  144. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js +2 -1
  145. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
  146. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  147. package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
  148. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  149. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js +1 -1
  150. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js.nft.json +1 -1
  151. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  152. package/web/.next/standalone/src/presentation/web/.next/server/app-paths-manifest.json +1 -0
  153. package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
  154. package/web/.next/standalone/src/presentation/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js +3 -0
  155. package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
  156. package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
  157. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  158. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  159. package/web/.next/{server/chunks/ssr/[root-of-the-server]__249c74f6._.js → standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js} +2 -2
  160. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
  161. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
  162. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  163. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  164. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  165. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  166. package/web/.next/standalone/src/presentation/web/app/api/agent-events/health/route.ts +69 -0
  167. package/web/.next/standalone/src/presentation/web/app/api/agent-events/route.ts +258 -54
  168. package/web/.next/standalone/src/presentation/web/app/layout.tsx +14 -0
  169. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +39 -2
  170. package/web/.next/standalone/src/presentation/web/components/common/repository-node/repository-node.tsx +1 -0
  171. package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/index.ts +1 -0
  172. package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.tsx +27 -0
  173. package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.tsx +21 -0
  174. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +15 -1
  175. package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +194 -31
  176. package/web/.next/standalone/src/presentation/web/components/layouts/app-shell/app-shell.tsx +3 -1
  177. package/web/.next/standalone/src/presentation/web/dev-server.ts +21 -0
  178. package/web/.next/standalone/src/presentation/web/hooks/use-agent-events.ts +161 -52
  179. package/web/.next/standalone/src/presentation/web/hooks/use-notifications.stories.tsx +39 -10
  180. package/web/.next/standalone/src/presentation/web/hooks/use-notifications.ts +68 -15
  181. package/web/.next/standalone/src/presentation/web/hooks/use-sound-enabled.ts +38 -0
  182. package/web/.next/standalone/src/presentation/web/hooks/use-sound.stories.tsx +109 -0
  183. package/web/.next/standalone/src/presentation/web/hooks/use-sound.ts +101 -0
  184. package/web/.next/standalone/src/presentation/web/public/agent-events-sw.js +164 -0
  185. package/web/.next/standalone/src/presentation/web/public/sounds/button.wav +0 -0
  186. package/web/.next/standalone/src/presentation/web/public/sounds/caution.wav +0 -0
  187. package/web/.next/standalone/src/presentation/web/public/sounds/celebration.wav +0 -0
  188. package/web/.next/standalone/src/presentation/web/public/sounds/disabled.wav +0 -0
  189. package/web/.next/standalone/src/presentation/web/public/sounds/notification.wav +0 -0
  190. package/web/.next/standalone/src/presentation/web/public/sounds/progress_loop.wav +0 -0
  191. package/web/.next/standalone/src/presentation/web/public/sounds/ringtone_loop.wav +0 -0
  192. package/web/.next/standalone/src/presentation/web/public/sounds/select.wav +0 -0
  193. package/web/.next/standalone/src/presentation/web/public/sounds/swipe.wav +0 -0
  194. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_01.wav +0 -0
  195. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_02.wav +0 -0
  196. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_03.wav +0 -0
  197. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_04.wav +0 -0
  198. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_05.wav +0 -0
  199. package/web/.next/standalone/src/presentation/web/public/sounds/tap_01.wav +0 -0
  200. package/web/.next/standalone/src/presentation/web/public/sounds/tap_02.wav +0 -0
  201. package/web/.next/standalone/src/presentation/web/public/sounds/tap_03.wav +0 -0
  202. package/web/.next/standalone/src/presentation/web/public/sounds/tap_04.wav +0 -0
  203. package/web/.next/standalone/src/presentation/web/public/sounds/tap_05.wav +0 -0
  204. package/web/.next/standalone/src/presentation/web/public/sounds/toggle_off.wav +0 -0
  205. package/web/.next/standalone/src/presentation/web/public/sounds/toggle_on.wav +0 -0
  206. package/web/.next/standalone/src/presentation/web/public/sounds/transition_down.wav +0 -0
  207. package/web/.next/standalone/src/presentation/web/public/sounds/transition_up.wav +0 -0
  208. package/web/.next/standalone/src/presentation/web/public/sounds/type_01.wav +0 -0
  209. package/web/.next/standalone/src/presentation/web/public/sounds/type_02.wav +0 -0
  210. package/web/.next/standalone/src/presentation/web/public/sounds/type_03.wav +0 -0
  211. package/web/.next/standalone/src/presentation/web/public/sounds/type_04.wav +0 -0
  212. package/web/.next/standalone/src/presentation/web/public/sounds/type_05.wav +0 -0
  213. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  214. package/web/.next/static/chunks/1f13bc20e938335c.css +2 -0
  215. package/web/.next/static/chunks/702384d364482e4b.js +10 -0
  216. package/web/.next/static/chunks/{68fb91b6eca14536.js → 8f4461aee9a05741.js} +2 -2
  217. package/web/.next/static/chunks/9a82d8c76f175235.js +1 -0
  218. package/web/.next/trace +1 -1
  219. package/web/.next/trace-build +1 -1
  220. package/web/.next/types/link.d.ts +1 -0
  221. package/web/.next/types/routes.d.ts +2 -1
  222. package/web/.next/types/validator.ts +9 -0
  223. package/web/public/agent-events-sw.js +164 -0
  224. package/web/public/sounds/button.wav +0 -0
  225. package/web/public/sounds/caution.wav +0 -0
  226. package/web/public/sounds/celebration.wav +0 -0
  227. package/web/public/sounds/disabled.wav +0 -0
  228. package/web/public/sounds/notification.wav +0 -0
  229. package/web/public/sounds/progress_loop.wav +0 -0
  230. package/web/public/sounds/ringtone_loop.wav +0 -0
  231. package/web/public/sounds/select.wav +0 -0
  232. package/web/public/sounds/swipe.wav +0 -0
  233. package/web/public/sounds/swipe_01.wav +0 -0
  234. package/web/public/sounds/swipe_02.wav +0 -0
  235. package/web/public/sounds/swipe_03.wav +0 -0
  236. package/web/public/sounds/swipe_04.wav +0 -0
  237. package/web/public/sounds/swipe_05.wav +0 -0
  238. package/web/public/sounds/tap_01.wav +0 -0
  239. package/web/public/sounds/tap_02.wav +0 -0
  240. package/web/public/sounds/tap_03.wav +0 -0
  241. package/web/public/sounds/tap_04.wav +0 -0
  242. package/web/public/sounds/tap_05.wav +0 -0
  243. package/web/public/sounds/toggle_off.wav +0 -0
  244. package/web/public/sounds/toggle_on.wav +0 -0
  245. package/web/public/sounds/transition_down.wav +0 -0
  246. package/web/public/sounds/transition_up.wav +0 -0
  247. package/web/public/sounds/type_01.wav +0 -0
  248. package/web/public/sounds/type_02.wav +0 -0
  249. package/web/public/sounds/type_03.wav +0 -0
  250. package/web/public/sounds/type_04.wav +0 -0
  251. package/web/public/sounds/type_05.wav +0 -0
  252. package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
  253. package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js.map +0 -1
  254. package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +0 -1
  255. package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
  256. package/web/.next/static/chunks/13d5334b17480a16.js +0 -10
  257. package/web/.next/static/chunks/16dec8021e251230.css +0 -2
  258. package/web/.next/static/chunks/22f30ce91a3c79c1.js +0 -1
  259. /package/web/.next/server/chunks/ssr/{[root-of-the-server]__249c74f6._.js.map → [root-of-the-server]__6e8b5181._.js.map} +0 -0
  260. /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → RwzxYU9D2fPc_4j5RYu0v}/_buildManifest.js +0 -0
  261. /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → RwzxYU9D2fPc_4j5RYu0v}/_clientMiddlewareManifest.json +0 -0
  262. /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → RwzxYU9D2fPc_4j5RYu0v}/_ssgManifest.js +0 -0
@@ -14,6 +14,11 @@ import { deleteFeature } from '@/app/actions/delete-feature';
14
14
  import { addRepository } from '@/app/actions/add-repository';
15
15
  import { deleteRepository } from '@/app/actions/delete-repository';
16
16
  import { useAgentEventsContext } from '@/hooks/agent-events-provider';
17
+ import { useSound } from '@/hooks/use-sound';
18
+ import {
19
+ mapEventTypeToState,
20
+ mapPhaseNameToLifecycle,
21
+ } from '@/components/common/feature-node/derive-feature-state';
17
22
 
18
23
  export interface ControlCenterState {
19
24
  nodes: CanvasNodeType[];
@@ -41,6 +46,7 @@ export interface ControlCenterState {
41
46
  sourceNodeId: string | null,
42
47
  dataOverride?: Partial<FeatureNodeData>
43
48
  ) => string;
49
+ selectFeatureById: (featureId: string) => void;
44
50
  }
45
51
 
46
52
  let nextFeatureId = 0;
@@ -67,6 +73,9 @@ export function useControlCenterState(
67
73
  const [selectedNode, setSelectedNode] = useState<FeatureNodeData | null>(null);
68
74
  const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false);
69
75
  const [isDeleting, setIsDeleting] = useState(false);
76
+ const deleteSound = useSound('transition_down', { volume: 0.5 });
77
+ const createSound = useSound('transition_up', { volume: 0.5 });
78
+ const clickSound = useSound('tap_01', { volume: 0.3 });
70
79
  const [pendingRepoNodeId, setPendingRepoNodeId] = useState<string | null>(null);
71
80
 
72
81
  // Sync server props into local state when router.refresh() delivers new data
@@ -79,6 +88,19 @@ export function useControlCenterState(
79
88
  .sort()
80
89
  .join(',');
81
90
 
91
+ // Track previous feature states so we can detect transitions on server refresh
92
+ const prevFeatureStatesRef = useRef<Map<string, FeatureNodeData['state']>>(new Map());
93
+
94
+ // Stable key that changes when feature DATA changes (state/lifecycle), not just IDs.
95
+ const initialDataKey = initialNodes
96
+ .filter((n) => n.type === 'featureNode')
97
+ .map((n) => {
98
+ const d = n.data as FeatureNodeData;
99
+ return `${n.id}:${d.state}:${d.lifecycle}`;
100
+ })
101
+ .sort()
102
+ .join(',');
103
+
82
104
  // Sync server props into local state — keyed by derived strings (initialNodeKey/initialEdgeKey)
83
105
  // to avoid infinite re-renders from unstable array references.
84
106
  useEffect(() => {
@@ -119,35 +141,148 @@ export function useControlCenterState(
119
141
  setEdges(initialEdges);
120
142
  }, [initialEdgeKey, initialEdges, setEdges]);
121
143
 
122
- // Refresh server data when SSE agent events arrive (status changes)
123
- const { lastEvent } = useAgentEventsContext();
124
- const processedEventRef = useRef<string | null>(null);
144
+ // Fallback notifications from server-refresh state transitions.
145
+ // Fires only when SSE didn't already deliver the matching event for a feature,
146
+ // e.g. when the SSE connection was down or the event was seeded into the cache.
147
+ const { events } = useAgentEventsContext();
125
148
 
126
149
  useEffect(() => {
127
- if (!lastEvent) return;
128
- const key = `${lastEvent.agentRunId}-${lastEvent.eventType}-${lastEvent.timestamp}`;
129
- if (processedEventRef.current === key) return;
130
- processedEventRef.current = key;
131
- router.refresh();
132
- }, [lastEvent, router]);
150
+ const prevStates = prevFeatureStatesRef.current;
133
151
 
134
- const onNodesChange = useCallback((changes: NodeChange<CanvasNodeType>[]) => {
135
- setNodes((ns) => applyNodeChanges(changes, ns));
136
- }, []);
152
+ for (const node of initialNodes) {
153
+ if (node.type !== 'featureNode') continue;
154
+ const data = node.data as FeatureNodeData;
155
+ const prev = prevStates.get(node.id);
156
+
157
+ if (prev !== undefined && prev !== data.state) {
158
+ // Check if SSE already delivered a matching event for this feature
159
+ const sseAlreadyCovered = events.some(
160
+ (e) => e.featureId === data.featureId && mapEventTypeToState(e.eventType) === data.state
161
+ );
162
+
163
+ if (!sseAlreadyCovered) {
164
+ if (data.state === 'done') {
165
+ toast.success(data.name, { description: 'Feature completed!' });
166
+ } else if (data.state === 'action-required') {
167
+ toast.warning(data.name, {
168
+ description: 'Waiting for your approval',
169
+ action: {
170
+ label: 'Review',
171
+ onClick: () => {
172
+ window.dispatchEvent(
173
+ new CustomEvent('shep:select-feature', {
174
+ detail: { featureId: data.featureId },
175
+ })
176
+ );
177
+ },
178
+ },
179
+ });
180
+ } else if (data.state === 'error') {
181
+ toast.error(data.name, { description: data.errorMessage ?? 'Agent failed' });
182
+ }
183
+ }
184
+ }
137
185
 
138
- const clearSelection = useCallback(() => {
139
- setSelectedNode(null);
186
+ prevStates.set(node.id, data.state);
187
+ }
188
+ }, [initialDataKey, initialNodes, events]);
189
+
190
+ // Targeted optimistic updates from SSE agent events + debounced reconciliation.
191
+ // Uses `events` array (not `lastEvent`) so that React batching cannot silently
192
+ // drop events when multiple SSE messages arrive in the same tick.
193
+ const processedEventCountRef = useRef(0);
194
+ const reconcileTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
195
+
196
+ useEffect(() => {
197
+ return () => clearTimeout(reconcileTimerRef.current);
140
198
  }, []);
141
199
 
142
- const handleNodeClick = useCallback((_event: React.MouseEvent, node: CanvasNodeType) => {
143
- if (node.type === 'featureNode') {
144
- const data = node.data as FeatureNodeData;
145
- if (data.state === 'creating') return;
146
- setIsCreateDrawerOpen(false);
147
- setSelectedNode(data);
200
+ useEffect(() => {
201
+ if (events.length <= processedEventCountRef.current) return;
202
+
203
+ const newEvents = events.slice(processedEventCountRef.current);
204
+ processedEventCountRef.current = events.length;
205
+
206
+ for (const event of newEvents) {
207
+ const newState = mapEventTypeToState(event.eventType);
208
+ const newLifecycle = mapPhaseNameToLifecycle(event.phaseName);
209
+
210
+ // Targeted node update — only clone the matched node
211
+ setNodes((prev) =>
212
+ prev.map((node) => {
213
+ if (node.type !== 'featureNode') return node;
214
+ const data = node.data as FeatureNodeData;
215
+ if (data.featureId !== event.featureId) return node;
216
+ return {
217
+ ...node,
218
+ data: {
219
+ ...data,
220
+ state: newState,
221
+ ...(newLifecycle !== undefined && { lifecycle: newLifecycle }),
222
+ },
223
+ };
224
+ })
225
+ );
226
+
227
+ // Update drawer if it's showing the matched feature
228
+ setSelectedNode((prev) => {
229
+ if (prev?.featureId !== event.featureId) return prev;
230
+ return {
231
+ ...prev,
232
+ state: newState,
233
+ ...(newLifecycle !== undefined && { lifecycle: newLifecycle }),
234
+ };
235
+ });
148
236
  }
237
+
238
+ // Debounced background reconciliation (3s after last SSE event)
239
+ clearTimeout(reconcileTimerRef.current);
240
+ reconcileTimerRef.current = setTimeout(() => router.refresh(), 3000);
241
+ }, [events, router]);
242
+
243
+ // Periodic polling fallback: refresh server data every 5s when any feature
244
+ // is in an active state (running/action-required). This ensures the UI stays
245
+ // current even if the SSE connection drops — belt-and-suspenders alongside SSE.
246
+ useEffect(() => {
247
+ const hasActiveFeature = nodes.some((n) => {
248
+ if (n.type !== 'featureNode') return false;
249
+ const data = n.data as FeatureNodeData;
250
+ return (
251
+ data.state === 'running' || data.state === 'action-required' || data.state === 'creating'
252
+ );
253
+ });
254
+
255
+ if (!hasActiveFeature) return;
256
+
257
+ const interval = setInterval(() => router.refresh(), 5_000);
258
+ return () => clearInterval(interval);
259
+ }, [nodes, router]);
260
+
261
+ const onNodesChange = useCallback((changes: NodeChange<CanvasNodeType>[]) => {
262
+ setNodes((ns) => applyNodeChanges(changes, ns));
149
263
  }, []);
150
264
 
265
+ const closeSound = useSound('transition_down', { volume: 0.01 });
266
+ const clearSelection = useCallback(() => {
267
+ setSelectedNode((prev) => {
268
+ if (prev) closeSound.play();
269
+ return null;
270
+ });
271
+ }, [closeSound]);
272
+
273
+ const handleNodeClick = useCallback(
274
+ (_event: React.MouseEvent, node: CanvasNodeType) => {
275
+ if (node.type === 'featureNode') {
276
+ const data = node.data as FeatureNodeData;
277
+ if (data.state === 'creating') return;
278
+ clickSound.play();
279
+ setIsCreateDrawerOpen(false);
280
+ setSelectedNode(data);
281
+ }
282
+ },
283
+ [clickSound]
284
+ );
285
+
151
286
  // Keyboard shortcut: Escape to deselect
152
287
  useEffect(() => {
153
288
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -319,9 +454,10 @@ export function useControlCenterState(
319
454
  );
320
455
 
321
456
  const handleAddFeature = useCallback(() => {
457
+ clickSound.play();
322
458
  setSelectedNode(null);
323
459
  setIsCreateDrawerOpen(true);
324
- }, []);
460
+ }, [clickSound]);
325
461
 
326
462
  const [pendingParentFeatureId, setPendingParentFeatureId] = useState<string | undefined>();
327
463
 
@@ -359,6 +495,7 @@ export function useControlCenterState(
359
495
  return;
360
496
  }
361
497
 
498
+ createSound.play();
362
499
  router.refresh();
363
500
  })
364
501
  .catch(() => {
@@ -368,13 +505,14 @@ export function useControlCenterState(
368
505
  toast.error('Failed to create feature');
369
506
  });
370
507
  },
371
- [router, createFeatureNode, pendingRepoNodeId, pendingParentFeatureId, setEdges]
508
+ [router, createFeatureNode, pendingRepoNodeId, createSound, pendingParentFeatureId, setEdges]
372
509
  );
373
510
 
374
511
  const closeCreateDrawer = useCallback(() => {
512
+ closeSound.play();
375
513
  setIsCreateDrawerOpen(false);
376
514
  setPendingParentFeatureId(undefined);
377
- }, []);
515
+ }, [closeSound]);
378
516
 
379
517
  const handleDeleteFeature = useCallback(
380
518
  async (featureId: string) => {
@@ -401,6 +539,7 @@ export function useControlCenterState(
401
539
  setEdges(result.edges);
402
540
  return result.nodes;
403
541
  });
542
+ deleteSound.play();
404
543
  toast.success('Feature deleted successfully');
405
544
  router.refresh();
406
545
  } catch {
@@ -409,7 +548,7 @@ export function useControlCenterState(
409
548
  setIsDeleting(false);
410
549
  }
411
550
  },
412
- [router, edges, setEdges]
551
+ [router, edges, deleteSound, setEdges]
413
552
  );
414
553
 
415
554
  const handleDeleteRepository = useCallback(
@@ -429,6 +568,7 @@ export function useControlCenterState(
429
568
  return;
430
569
  }
431
570
 
571
+ deleteSound.play();
432
572
  toast.success('Repository removed');
433
573
  router.refresh();
434
574
  } catch {
@@ -436,14 +576,18 @@ export function useControlCenterState(
436
576
  router.refresh();
437
577
  }
438
578
  },
439
- [router, setEdges]
579
+ [router, deleteSound, setEdges]
440
580
  );
441
581
 
442
- const handleAddFeatureToRepo = useCallback((repoNodeId: string) => {
443
- setSelectedNode(null);
444
- setPendingRepoNodeId(repoNodeId);
445
- setIsCreateDrawerOpen(true);
446
- }, []);
582
+ const handleAddFeatureToRepo = useCallback(
583
+ (repoNodeId: string) => {
584
+ clickSound.play();
585
+ setSelectedNode(null);
586
+ setPendingRepoNodeId(repoNodeId);
587
+ setIsCreateDrawerOpen(true);
588
+ },
589
+ [clickSound]
590
+ );
447
591
 
448
592
  const handleAddFeatureToFeature = useCallback(
449
593
  (featureNodeId: string) => {
@@ -565,6 +709,7 @@ export function useControlCenterState(
565
709
  }))
566
710
  );
567
711
 
712
+ createSound.play();
568
713
  router.refresh();
569
714
  })
570
715
  .catch(() => {
@@ -581,9 +726,26 @@ export function useControlCenterState(
581
726
  toast.error('Failed to add repository');
582
727
  });
583
728
  },
584
- [router, setEdges]
729
+ [router, createSound, setEdges]
585
730
  );
586
731
 
732
+ // Ref keeps latest nodes so selectFeatureById has a stable identity
733
+ const nodesRef = useRef(nodes);
734
+ nodesRef.current = nodes;
735
+
736
+ const selectFeatureById = useCallback((featureId: string) => {
737
+ const node = nodesRef.current.find(
738
+ (n) => n.type === 'featureNode' && (n.data as FeatureNodeData).featureId === featureId
739
+ );
740
+ if (node) {
741
+ const data = node.data as FeatureNodeData;
742
+ if (data.state !== 'creating') {
743
+ setIsCreateDrawerOpen(false);
744
+ setSelectedNode(data);
745
+ }
746
+ }
747
+ }, []);
748
+
587
749
  const pendingNode = pendingRepoNodeId ? nodes.find((n) => n.id === pendingRepoNodeId) : null;
588
750
  const pendingRepositoryPath =
589
751
  (pendingNode?.data as { repositoryPath?: string } | undefined)?.repositoryPath ?? '';
@@ -610,5 +772,6 @@ export function useControlCenterState(
610
772
  handleDeleteRepository,
611
773
  isDeleting,
612
774
  createFeatureNode,
775
+ selectFeatureById,
613
776
  };
614
777
  }
@@ -4,6 +4,7 @@ import { useCallback, type ReactNode } from 'react';
4
4
  import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
5
5
  import { AppSidebar } from '@/components/layouts/app-sidebar';
6
6
  import { ThemeToggle } from '@/components/common/theme-toggle';
7
+ import { SoundToggle } from '@/components/common/sound-toggle';
7
8
  import { AgentEventsProvider } from '@/hooks/agent-events-provider';
8
9
  import { useNotifications } from '@/hooks/use-notifications';
9
10
 
@@ -25,7 +26,8 @@ function AppShellInner({ children }: AppShellProps) {
25
26
  <AppSidebar features={[]} onNewFeature={handleNewFeature} />
26
27
  <SidebarInset>
27
28
  <div className="relative h-full">
28
- <div className="absolute top-3 right-3 z-50">
29
+ <div className="absolute top-3 right-3 z-50 flex gap-1">
30
+ <SoundToggle />
29
31
  <ThemeToggle />
30
32
  </div>
31
33
  <main className="h-full">{children}</main>
@@ -20,6 +20,14 @@ import path from 'node:path';
20
20
  import { initializeContainer, container } from '@/infrastructure/di/container.js';
21
21
  import { InitializeSettingsUseCase } from '@/application/use-cases/settings/initialize-settings.use-case.js';
22
22
  import { initializeSettings } from '@/infrastructure/services/settings.service.js';
23
+ import type { IAgentRunRepository } from '@/application/ports/output/agents/agent-run-repository.interface.js';
24
+ import type { IPhaseTimingRepository } from '@/application/ports/output/agents/phase-timing-repository.interface.js';
25
+ import type { IFeatureRepository } from '@/application/ports/output/repositories/feature-repository.interface.js';
26
+ import type { INotificationService } from '@/application/ports/output/services/notification-service.interface.js';
27
+ import {
28
+ initializeNotificationWatcher,
29
+ getNotificationWatcher,
30
+ } from '@/infrastructure/services/notifications/notification-watcher.service.js';
23
31
 
24
32
  const DEFAULT_PORT = 3000;
25
33
 
@@ -62,6 +70,14 @@ async function main() {
62
70
  const initSettingsUseCase = container.resolve(InitializeSettingsUseCase);
63
71
  const settings = await initSettingsUseCase.execute();
64
72
  initializeSettings(settings);
73
+
74
+ // Start notification watcher for real-time SSE events (same as shep ui)
75
+ const runRepo = container.resolve<IAgentRunRepository>('IAgentRunRepository');
76
+ const phaseTimingRepo = container.resolve<IPhaseTimingRepository>('IPhaseTimingRepository');
77
+ const featureRepo = container.resolve<IFeatureRepository>('IFeatureRepository');
78
+ const notificationService = container.resolve<INotificationService>('INotificationService');
79
+ initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
80
+ getNotificationWatcher().start();
65
81
  } catch (error) {
66
82
  console.warn('[dev-server] DI initialization failed — features will be empty:', error);
67
83
  }
@@ -104,6 +120,11 @@ async function main() {
104
120
  console.log('\n[dev-server] Shutting down...');
105
121
  const forceExit = setTimeout(() => process.exit(0), 2000);
106
122
  try {
123
+ try {
124
+ getNotificationWatcher().stop();
125
+ } catch {
126
+ /* not initialized */
127
+ }
107
128
  server.closeAllConnections();
108
129
  await Promise.all([
109
130
  new Promise<void>((resolve) => server.close(() => resolve())),
@@ -15,66 +15,178 @@ export interface UseAgentEventsResult {
15
15
  connectionStatus: ConnectionStatus;
16
16
  }
17
17
 
18
- const BASE_BACKOFF_MS = 1000;
19
- const MAX_BACKOFF_MS = 30_000;
20
- /** Only reset backoff after connection is stable for this long */
21
- const STABLE_CONNECTION_MS = 5_000;
22
-
18
+ const SW_PATH = '/agent-events-sw.js';
19
+
20
+ /**
21
+ * Hook that receives real-time agent notification events via a Service Worker.
22
+ *
23
+ * The SW maintains a single SSE connection to /api/agent-events and
24
+ * broadcasts events to all open tabs. Falls back to direct EventSource
25
+ * when Service Workers are unavailable.
26
+ */
23
27
  export function useAgentEvents(options?: UseAgentEventsOptions): UseAgentEventsResult {
24
28
  const [events, setEvents] = useState<NotificationEvent[]>([]);
25
29
  const [lastEvent, setLastEvent] = useState<NotificationEvent | null>(null);
26
30
  const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
27
31
 
28
- const backoffRef = useRef(BASE_BACKOFF_MS);
29
- const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
30
- const stableTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
31
- const esRef = useRef<EventSource | null>(null);
32
-
33
32
  const runId = options?.runId;
33
+ const swRef = useRef<ServiceWorker | null>(null);
34
+
35
+ const onMessage = useCallback((event: MessageEvent) => {
36
+ const msg = event.data;
37
+ if (!msg || typeof msg !== 'object') return;
38
+
39
+ if (msg.type === 'notification') {
40
+ const parsed = msg.data as NotificationEvent;
41
+ setEvents((prev) => [...prev, parsed]);
42
+ setLastEvent(parsed);
43
+ } else if (msg.type === 'status') {
44
+ setConnectionStatus(msg.status as ConnectionStatus);
45
+ }
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ if (typeof window === 'undefined') return;
50
+
51
+ // ?sse=direct bypasses the Service Worker for debugging
52
+ const bypassSW =
53
+ typeof location !== 'undefined' && new URLSearchParams(location.search).has('sse');
54
+
55
+ // Fallback: direct EventSource when SW not supported or bypassed
56
+ if (!navigator.serviceWorker || bypassSW) {
57
+ return connectDirectEventSource(runId, setEvents, setLastEvent, setConnectionStatus);
58
+ }
59
+
60
+ let cancelled = false;
61
+ const fallbackCleanupRef = { current: undefined as (() => void) | undefined };
34
62
 
35
- const connect = useCallback(() => {
36
- if (typeof window === 'undefined' || typeof EventSource === 'undefined') {
37
- return;
63
+ function subscribeToWorker(worker: ServiceWorker) {
64
+ if (cancelled) return;
65
+ swRef.current = worker;
66
+ worker.postMessage({ type: 'subscribe', runId });
67
+ setConnectionStatus('connecting');
38
68
  }
39
69
 
40
- // Prevent duplicate connections
41
- if (esRef.current) {
42
- return;
70
+ // Listen for messages from whatever SW controls this page
71
+ navigator.serviceWorker.addEventListener('message', onMessage);
72
+
73
+ // Re-subscribe when SW controller changes (e.g., after SW update via skipWaiting)
74
+ function handleControllerChange() {
75
+ if (cancelled) return;
76
+ const newController = navigator.serviceWorker.controller;
77
+ if (newController) {
78
+ swRef.current = newController;
79
+ newController.postMessage({ type: 'subscribe', runId });
80
+ setConnectionStatus('connecting');
81
+ }
43
82
  }
83
+ navigator.serviceWorker.addEventListener('controllerchange', handleControllerChange);
84
+
85
+ navigator.serviceWorker
86
+ .register(SW_PATH, { scope: '/' })
87
+ .then((registration) => {
88
+ if (cancelled) return;
89
+
90
+ // Use the active worker, or wait for it to activate
91
+ const sw = registration.active ?? registration.installing ?? registration.waiting;
92
+ if (!sw) {
93
+ // No worker at all — fall back to direct EventSource
94
+ fallbackCleanupRef.current = connectDirectEventSource(
95
+ runId,
96
+ setEvents,
97
+ setLastEvent,
98
+ setConnectionStatus
99
+ );
100
+ return;
101
+ }
102
+
103
+ if (sw.state === 'activated') {
104
+ subscribeToWorker(sw);
105
+ } else {
106
+ sw.addEventListener('statechange', () => {
107
+ if (sw.state === 'activated') subscribeToWorker(sw);
108
+ });
109
+ }
110
+ })
111
+ .catch(() => {
112
+ // SW registration failed — fall back to direct EventSource
113
+ if (!cancelled) {
114
+ fallbackCleanupRef.current = connectDirectEventSource(
115
+ runId,
116
+ setEvents,
117
+ setLastEvent,
118
+ setConnectionStatus
119
+ );
120
+ }
121
+ });
122
+
123
+ return () => {
124
+ cancelled = true;
125
+ navigator.serviceWorker.removeEventListener('message', onMessage);
126
+ navigator.serviceWorker.removeEventListener('controllerchange', handleControllerChange);
127
+ swRef.current?.postMessage({ type: 'unsubscribe' });
128
+ swRef.current = null;
129
+ fallbackCleanupRef.current?.();
130
+ };
131
+ }, [runId, onMessage]);
132
+
133
+ return { events, lastEvent, connectionStatus };
134
+ }
135
+
136
+ // --- EventSource fallback (identical to the previous implementation) ---
137
+
138
+ const BASE_BACKOFF_MS = 1000;
139
+ const MAX_BACKOFF_MS = 30_000;
140
+ const STABLE_CONNECTION_MS = 5_000;
141
+
142
+ function connectDirectEventSource(
143
+ runId: string | undefined,
144
+ setEvents: React.Dispatch<React.SetStateAction<NotificationEvent[]>>,
145
+ setLastEvent: React.Dispatch<React.SetStateAction<NotificationEvent | null>>,
146
+ setConnectionStatus: React.Dispatch<React.SetStateAction<ConnectionStatus>>
147
+ ): () => void {
148
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
149
+ if (typeof EventSource === 'undefined') return () => {};
150
+
151
+ let es: EventSource | null = null;
152
+ let stopped = false;
153
+ let backoff = BASE_BACKOFF_MS;
154
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
155
+ let stableTimer: ReturnType<typeof setTimeout> | null = null;
156
+
157
+ function connect() {
158
+ if (stopped) return;
44
159
 
45
160
  const url = runId
46
161
  ? `/api/agent-events?runId=${encodeURIComponent(runId)}`
47
162
  : '/api/agent-events';
48
163
 
49
- const es = new EventSource(url);
50
- esRef.current = es;
164
+ es = new EventSource(url);
51
165
  setConnectionStatus('connecting');
52
166
 
53
167
  es.onopen = () => {
54
168
  setConnectionStatus('connected');
55
- // Only reset backoff after connection stays open for a while
56
- stableTimerRef.current = setTimeout(() => {
57
- stableTimerRef.current = null;
58
- backoffRef.current = BASE_BACKOFF_MS;
169
+ stableTimer = setTimeout(() => {
170
+ stableTimer = null;
171
+ backoff = BASE_BACKOFF_MS;
59
172
  }, STABLE_CONNECTION_MS);
60
173
  };
61
174
 
62
175
  es.onerror = () => {
63
- es.close();
64
- esRef.current = null;
176
+ es?.close();
177
+ es = null;
65
178
  setConnectionStatus('disconnected');
66
179
 
67
- // Cancel stable timer — connection wasn't stable
68
- if (stableTimerRef.current !== null) {
69
- clearTimeout(stableTimerRef.current);
70
- stableTimerRef.current = null;
180
+ if (stableTimer !== null) {
181
+ clearTimeout(stableTimer);
182
+ stableTimer = null;
71
183
  }
72
184
 
73
- const delay = backoffRef.current;
74
- backoffRef.current = Math.min(delay * 2, MAX_BACKOFF_MS);
185
+ const delay = backoff;
186
+ backoff = Math.min(delay * 2, MAX_BACKOFF_MS);
75
187
 
76
- reconnectTimerRef.current = setTimeout(() => {
77
- reconnectTimerRef.current = null;
188
+ reconnectTimer = setTimeout(() => {
189
+ reconnectTimer = null;
78
190
  connect();
79
191
  }, delay);
80
192
  };
@@ -84,26 +196,23 @@ export function useAgentEvents(options?: UseAgentEventsOptions): UseAgentEventsR
84
196
  setEvents((prev) => [...prev, parsed]);
85
197
  setLastEvent(parsed);
86
198
  }) as EventListener);
87
- }, [runId]);
88
-
89
- useEffect(() => {
90
- connect();
199
+ }
91
200
 
92
- return () => {
93
- if (reconnectTimerRef.current !== null) {
94
- clearTimeout(reconnectTimerRef.current);
95
- reconnectTimerRef.current = null;
96
- }
97
- if (stableTimerRef.current !== null) {
98
- clearTimeout(stableTimerRef.current);
99
- stableTimerRef.current = null;
100
- }
101
- if (esRef.current) {
102
- esRef.current.close();
103
- esRef.current = null;
104
- }
105
- };
106
- }, [connect]);
201
+ connect();
107
202
 
108
- return { events, lastEvent, connectionStatus };
203
+ return () => {
204
+ stopped = true;
205
+ if (reconnectTimer !== null) {
206
+ clearTimeout(reconnectTimer);
207
+ reconnectTimer = null;
208
+ }
209
+ if (stableTimer !== null) {
210
+ clearTimeout(stableTimer);
211
+ stableTimer = null;
212
+ }
213
+ if (es) {
214
+ es.close();
215
+ es = null;
216
+ }
217
+ };
109
218
  }