@shepai/cli 1.53.2 → 1.55.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 (298) hide show
  1. package/README.md +22 -8
  2. package/apis/json-schema/NotificationEvent.yaml +4 -0
  3. package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts +49 -0
  4. package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts.map +1 -0
  5. package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.js +7 -0
  6. package/dist/packages/core/src/application/ports/output/services/index.d.ts +1 -0
  7. package/dist/packages/core/src/application/ports/output/services/index.d.ts.map +1 -1
  8. package/dist/packages/core/src/domain/generated/output.d.ts +4 -0
  9. package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
  10. package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
  11. package/dist/packages/core/src/infrastructure/di/container.js +2 -0
  12. package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts +1 -0
  13. package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts.map +1 -1
  14. package/dist/packages/core/src/infrastructure/services/browser-opener.service.js +4 -0
  15. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts +16 -0
  16. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts.map +1 -0
  17. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.js +72 -0
  18. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts +7 -0
  19. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts.map +1 -1
  20. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.js +9 -0
  21. package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts +4 -0
  22. package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts.map +1 -1
  23. package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.js +19 -6
  24. package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts +5 -3
  25. package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts.map +1 -1
  26. package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.js +21 -13
  27. package/dist/src/presentation/cli/commands/_serve.command.d.ts +32 -0
  28. package/dist/src/presentation/cli/commands/_serve.command.d.ts.map +1 -0
  29. package/dist/src/presentation/cli/commands/_serve.command.js +91 -0
  30. package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts +27 -0
  31. package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts.map +1 -0
  32. package/dist/src/presentation/cli/commands/daemon/start-daemon.js +135 -0
  33. package/dist/src/presentation/cli/commands/start.command.d.ts +15 -0
  34. package/dist/src/presentation/cli/commands/start.command.d.ts.map +1 -0
  35. package/dist/src/presentation/cli/commands/start.command.js +33 -0
  36. package/dist/src/presentation/cli/commands/status.command.d.ts +20 -0
  37. package/dist/src/presentation/cli/commands/status.command.d.ts.map +1 -0
  38. package/dist/src/presentation/cli/commands/status.command.js +119 -0
  39. package/dist/src/presentation/cli/commands/stop.command.d.ts +22 -0
  40. package/dist/src/presentation/cli/commands/stop.command.d.ts.map +1 -0
  41. package/dist/src/presentation/cli/commands/stop.command.js +79 -0
  42. package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
  43. package/dist/src/presentation/cli/commands/ui.command.js +2 -1
  44. package/dist/src/presentation/cli/index.d.ts +5 -2
  45. package/dist/src/presentation/cli/index.d.ts.map +1 -1
  46. package/dist/src/presentation/cli/index.js +23 -4
  47. package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts +13 -0
  48. package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts.map +1 -0
  49. package/dist/src/presentation/web/app/api/agent-events/health/route.js +60 -0
  50. package/dist/src/presentation/web/app/api/agent-events/route.d.ts +8 -4
  51. package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -1
  52. package/dist/src/presentation/web/app/api/agent-events/route.js +227 -55
  53. package/dist/src/presentation/web/app/layout.d.ts.map +1 -1
  54. package/dist/src/presentation/web/app/layout.js +14 -0
  55. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts +5 -1
  56. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  57. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +29 -1
  58. package/dist/src/presentation/web/components/common/repository-node/repository-node.d.ts.map +1 -1
  59. package/dist/src/presentation/web/components/common/repository-node/repository-node.js +1 -1
  60. package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts +2 -0
  61. package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts.map +1 -0
  62. package/dist/src/presentation/web/components/common/sound-toggle/index.js +1 -0
  63. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts +2 -0
  64. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts.map +1 -0
  65. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.js +9 -0
  66. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts +8 -0
  67. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts.map +1 -0
  68. package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.js +17 -0
  69. package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
  70. package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +14 -2
  71. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +1 -0
  72. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
  73. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +151 -19
  74. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.d.ts.map +1 -1
  75. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +2 -1
  76. package/dist/src/presentation/web/dev-server.js +14 -0
  77. package/dist/src/presentation/web/hooks/use-agent-events.d.ts +7 -0
  78. package/dist/src/presentation/web/hooks/use-agent-events.d.ts.map +1 -1
  79. package/dist/src/presentation/web/hooks/use-agent-events.js +135 -47
  80. package/dist/src/presentation/web/hooks/use-notifications.d.ts.map +1 -1
  81. package/dist/src/presentation/web/hooks/use-notifications.js +56 -16
  82. package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts.map +1 -1
  83. package/dist/src/presentation/web/hooks/use-notifications.stories.js +28 -9
  84. package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts +6 -0
  85. package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts.map +1 -0
  86. package/dist/src/presentation/web/hooks/use-sound-enabled.js +27 -0
  87. package/dist/src/presentation/web/hooks/use-sound.d.ts +15 -0
  88. package/dist/src/presentation/web/hooks/use-sound.d.ts.map +1 -0
  89. package/dist/src/presentation/web/hooks/use-sound.js +83 -0
  90. package/dist/src/presentation/web/hooks/use-sound.stories.d.ts +8 -0
  91. package/dist/src/presentation/web/hooks/use-sound.stories.d.ts.map +1 -0
  92. package/dist/src/presentation/web/hooks/use-sound.stories.js +67 -0
  93. package/dist/src/presentation/web/public/agent-events-sw.d.ts +35 -0
  94. package/dist/src/presentation/web/public/agent-events-sw.d.ts.map +1 -0
  95. package/dist/src/presentation/web/public/agent-events-sw.js +140 -0
  96. package/dist/tsconfig.build.tsbuildinfo +1 -1
  97. package/package.json +1 -1
  98. package/web/.next/BUILD_ID +1 -1
  99. package/web/.next/app-path-routes-manifest.json +1 -0
  100. package/web/.next/build-manifest.json +2 -2
  101. package/web/.next/cache/.previewinfo +1 -1
  102. package/web/.next/cache/.rscinfo +1 -1
  103. package/web/.next/cache/.tsbuildinfo +1 -1
  104. package/web/.next/cache/config.json +3 -3
  105. package/web/.next/fallback-build-manifest.json +2 -2
  106. package/web/.next/prerender-manifest.json +3 -3
  107. package/web/.next/required-server-files.js +1 -1
  108. package/web/.next/required-server-files.json +1 -1
  109. package/web/.next/routes-manifest.json +6 -0
  110. package/web/.next/server/app/_global-error.html +2 -2
  111. package/web/.next/server/app/_global-error.rsc +1 -1
  112. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  113. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  114. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  115. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  116. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  117. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  118. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  119. package/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
  120. package/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
  121. package/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
  122. package/web/.next/server/app/api/agent-events/health/route.js +6 -0
  123. package/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
  124. package/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
  125. package/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
  126. package/web/.next/server/app/api/agent-events/route.js +2 -1
  127. package/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
  128. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  129. package/web/.next/server/app/page.js.nft.json +1 -1
  130. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  131. package/web/.next/server/app/version/page.js +1 -1
  132. package/web/.next/server/app/version/page.js.nft.json +1 -1
  133. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  134. package/web/.next/server/app-paths-manifest.json +1 -0
  135. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
  136. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -0
  137. package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js +3 -0
  138. package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js.map +1 -0
  139. package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
  140. package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js.map +1 -0
  141. package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
  142. package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js.map +1 -0
  143. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  144. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  145. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  146. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
  147. 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
  148. package/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
  149. package/web/.next/server/chunks/ssr/_1632a21f._.js.map +1 -0
  150. package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
  151. package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js.map +1 -1
  152. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  153. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  154. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  155. package/web/.next/server/pages/500.html +2 -2
  156. package/web/.next/server/server-reference-manifest.js +1 -1
  157. package/web/.next/server/server-reference-manifest.json +15 -15
  158. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  159. package/web/.next/standalone/src/presentation/web/.next/app-path-routes-manifest.json +1 -0
  160. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  161. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  162. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  163. package/web/.next/standalone/src/presentation/web/.next/routes-manifest.json +6 -0
  164. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  165. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  166. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  167. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  168. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  169. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  170. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  171. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  172. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  173. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
  174. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
  175. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
  176. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js +6 -0
  177. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
  178. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
  179. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
  180. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js +2 -1
  181. package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
  182. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  183. package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
  184. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  185. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js +1 -1
  186. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js.nft.json +1 -1
  187. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  188. package/web/.next/standalone/src/presentation/web/.next/server/app-paths-manifest.json +1 -0
  189. package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
  190. 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
  191. package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
  192. package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
  193. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  194. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  195. 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
  196. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
  197. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
  198. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  199. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  200. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  201. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  202. package/web/.next/standalone/src/presentation/web/app/api/agent-events/health/route.ts +69 -0
  203. package/web/.next/standalone/src/presentation/web/app/api/agent-events/route.ts +258 -54
  204. package/web/.next/standalone/src/presentation/web/app/layout.tsx +14 -0
  205. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +39 -2
  206. package/web/.next/standalone/src/presentation/web/components/common/repository-node/repository-node.tsx +1 -0
  207. package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/index.ts +1 -0
  208. package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.tsx +27 -0
  209. package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.tsx +21 -0
  210. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +15 -1
  211. package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +194 -31
  212. package/web/.next/standalone/src/presentation/web/components/layouts/app-shell/app-shell.tsx +3 -1
  213. package/web/.next/standalone/src/presentation/web/dev-server.ts +21 -0
  214. package/web/.next/standalone/src/presentation/web/hooks/use-agent-events.ts +161 -52
  215. package/web/.next/standalone/src/presentation/web/hooks/use-notifications.stories.tsx +39 -10
  216. package/web/.next/standalone/src/presentation/web/hooks/use-notifications.ts +68 -15
  217. package/web/.next/standalone/src/presentation/web/hooks/use-sound-enabled.ts +38 -0
  218. package/web/.next/standalone/src/presentation/web/hooks/use-sound.stories.tsx +109 -0
  219. package/web/.next/standalone/src/presentation/web/hooks/use-sound.ts +101 -0
  220. package/web/.next/standalone/src/presentation/web/public/agent-events-sw.js +164 -0
  221. package/web/.next/standalone/src/presentation/web/public/sounds/button.wav +0 -0
  222. package/web/.next/standalone/src/presentation/web/public/sounds/caution.wav +0 -0
  223. package/web/.next/standalone/src/presentation/web/public/sounds/celebration.wav +0 -0
  224. package/web/.next/standalone/src/presentation/web/public/sounds/disabled.wav +0 -0
  225. package/web/.next/standalone/src/presentation/web/public/sounds/notification.wav +0 -0
  226. package/web/.next/standalone/src/presentation/web/public/sounds/progress_loop.wav +0 -0
  227. package/web/.next/standalone/src/presentation/web/public/sounds/ringtone_loop.wav +0 -0
  228. package/web/.next/standalone/src/presentation/web/public/sounds/select.wav +0 -0
  229. package/web/.next/standalone/src/presentation/web/public/sounds/swipe.wav +0 -0
  230. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_01.wav +0 -0
  231. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_02.wav +0 -0
  232. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_03.wav +0 -0
  233. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_04.wav +0 -0
  234. package/web/.next/standalone/src/presentation/web/public/sounds/swipe_05.wav +0 -0
  235. package/web/.next/standalone/src/presentation/web/public/sounds/tap_01.wav +0 -0
  236. package/web/.next/standalone/src/presentation/web/public/sounds/tap_02.wav +0 -0
  237. package/web/.next/standalone/src/presentation/web/public/sounds/tap_03.wav +0 -0
  238. package/web/.next/standalone/src/presentation/web/public/sounds/tap_04.wav +0 -0
  239. package/web/.next/standalone/src/presentation/web/public/sounds/tap_05.wav +0 -0
  240. package/web/.next/standalone/src/presentation/web/public/sounds/toggle_off.wav +0 -0
  241. package/web/.next/standalone/src/presentation/web/public/sounds/toggle_on.wav +0 -0
  242. package/web/.next/standalone/src/presentation/web/public/sounds/transition_down.wav +0 -0
  243. package/web/.next/standalone/src/presentation/web/public/sounds/transition_up.wav +0 -0
  244. package/web/.next/standalone/src/presentation/web/public/sounds/type_01.wav +0 -0
  245. package/web/.next/standalone/src/presentation/web/public/sounds/type_02.wav +0 -0
  246. package/web/.next/standalone/src/presentation/web/public/sounds/type_03.wav +0 -0
  247. package/web/.next/standalone/src/presentation/web/public/sounds/type_04.wav +0 -0
  248. package/web/.next/standalone/src/presentation/web/public/sounds/type_05.wav +0 -0
  249. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  250. package/web/.next/static/chunks/1f13bc20e938335c.css +2 -0
  251. package/web/.next/static/chunks/3eaa6f2966e27b8f.js +10 -0
  252. package/web/.next/static/chunks/{68fb91b6eca14536.js → 8f4461aee9a05741.js} +2 -2
  253. package/web/.next/static/chunks/9a82d8c76f175235.js +1 -0
  254. package/web/.next/trace +1 -1
  255. package/web/.next/trace-build +1 -1
  256. package/web/.next/types/link.d.ts +1 -0
  257. package/web/.next/types/routes.d.ts +2 -1
  258. package/web/.next/types/validator.ts +9 -0
  259. package/web/public/agent-events-sw.js +164 -0
  260. package/web/public/sounds/button.wav +0 -0
  261. package/web/public/sounds/caution.wav +0 -0
  262. package/web/public/sounds/celebration.wav +0 -0
  263. package/web/public/sounds/disabled.wav +0 -0
  264. package/web/public/sounds/notification.wav +0 -0
  265. package/web/public/sounds/progress_loop.wav +0 -0
  266. package/web/public/sounds/ringtone_loop.wav +0 -0
  267. package/web/public/sounds/select.wav +0 -0
  268. package/web/public/sounds/swipe.wav +0 -0
  269. package/web/public/sounds/swipe_01.wav +0 -0
  270. package/web/public/sounds/swipe_02.wav +0 -0
  271. package/web/public/sounds/swipe_03.wav +0 -0
  272. package/web/public/sounds/swipe_04.wav +0 -0
  273. package/web/public/sounds/swipe_05.wav +0 -0
  274. package/web/public/sounds/tap_01.wav +0 -0
  275. package/web/public/sounds/tap_02.wav +0 -0
  276. package/web/public/sounds/tap_03.wav +0 -0
  277. package/web/public/sounds/tap_04.wav +0 -0
  278. package/web/public/sounds/tap_05.wav +0 -0
  279. package/web/public/sounds/toggle_off.wav +0 -0
  280. package/web/public/sounds/toggle_on.wav +0 -0
  281. package/web/public/sounds/transition_down.wav +0 -0
  282. package/web/public/sounds/transition_up.wav +0 -0
  283. package/web/public/sounds/type_01.wav +0 -0
  284. package/web/public/sounds/type_02.wav +0 -0
  285. package/web/public/sounds/type_03.wav +0 -0
  286. package/web/public/sounds/type_04.wav +0 -0
  287. package/web/public/sounds/type_05.wav +0 -0
  288. package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
  289. package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js.map +0 -1
  290. package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +0 -1
  291. package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
  292. package/web/.next/static/chunks/13d5334b17480a16.js +0 -10
  293. package/web/.next/static/chunks/16dec8021e251230.css +0 -2
  294. package/web/.next/static/chunks/22f30ce91a3c79c1.js +0 -1
  295. /package/web/.next/server/chunks/ssr/{[root-of-the-server]__249c74f6._.js.map → [root-of-the-server]__6e8b5181._.js.map} +0 -0
  296. /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → JmDtIt3S6EfLuRKwlzJrW}/_buildManifest.js +0 -0
  297. /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → JmDtIt3S6EfLuRKwlzJrW}/_clientMiddlewareManifest.json +0 -0
  298. /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → JmDtIt3S6EfLuRKwlzJrW}/_ssgManifest.js +0 -0
@@ -1,12 +1,20 @@
1
- import { useState } from 'react';
1
+ import { useCallback, useMemo, useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { toast } from 'sonner';
4
4
  import { Toaster } from '@/components/ui/sonner';
5
5
  import { Button } from '@/components/ui/button';
6
+ import { useSound } from './use-sound';
6
7
 
7
8
  /** Mirrors NotificationSeverity enum values (inlined to avoid @shepai/core import in Storybook). */
8
9
  type Severity = 'info' | 'warning' | 'success' | 'error';
9
10
 
11
+ const SEVERITY_SOUNDS: Record<Severity, string> = {
12
+ success: 'celebration',
13
+ error: 'caution',
14
+ warning: 'notification',
15
+ info: 'button',
16
+ };
17
+
10
18
  interface DemoEvent {
11
19
  eventType: string;
12
20
  severity: Severity;
@@ -15,16 +23,37 @@ interface DemoEvent {
15
23
  phaseName?: string;
16
24
  }
17
25
 
18
- function simulateNotification(event: DemoEvent): void {
19
- const method: 'success' | 'error' | 'warning' | 'info' = event.severity ?? 'info';
20
- toast[method](event.featureName, { description: event.message });
26
+ function NotificationDemo() {
27
+ const successSound = useSound('celebration', { volume: 0.5 });
28
+ const errorSound = useSound('caution', { volume: 0.5 });
29
+ const warningSound = useSound('notification', { volume: 0.5 });
30
+ const infoSound = useSound('button', { volume: 0.5 });
21
31
 
22
- if (globalThis.Notification?.permission === 'granted') {
23
- new Notification(event.featureName, { body: event.message });
24
- }
25
- }
32
+ const soundsByName = useMemo<Record<string, { play: () => void }>>(
33
+ () => ({
34
+ celebration: successSound,
35
+ caution: errorSound,
36
+ notification: warningSound,
37
+ button: infoSound,
38
+ }),
39
+ [successSound, errorSound, warningSound, infoSound]
40
+ );
41
+
42
+ const simulateNotification = useCallback(
43
+ (event: DemoEvent) => {
44
+ const method = event.severity ?? 'info';
45
+ toast[method](event.featureName, { description: event.message });
46
+
47
+ if (globalThis.Notification?.permission === 'granted') {
48
+ new Notification(event.featureName, { body: event.message });
49
+ }
50
+
51
+ const soundName = SEVERITY_SOUNDS[event.severity];
52
+ soundsByName[soundName]?.play();
53
+ },
54
+ [soundsByName]
55
+ );
26
56
 
27
- function NotificationDemo() {
28
57
  const [browserPermission, setBrowserPermission] = useState<NotificationPermission>(
29
58
  typeof globalThis.Notification !== 'undefined' ? globalThis.Notification.permission : 'default'
30
59
  );
@@ -52,7 +81,7 @@ function NotificationDemo() {
52
81
  <div className="flex flex-col gap-2">
53
82
  <h3 className="text-sm font-semibold">Simulate Notification Events</h3>
54
83
  <p className="text-muted-foreground text-xs">
55
- Click a button to fire a toast and browser notification.
84
+ Click a button to fire a toast, browser notification, and sound.
56
85
  </p>
57
86
  <div className="flex flex-wrap gap-2">
58
87
  <Button
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { toast } from 'sonner';
5
5
  import type { NotificationEvent } from '@shepai/core/domain/generated/output';
6
- import { NotificationSeverity } from '@shepai/core/domain/generated/output';
6
+ import { NotificationEventType, NotificationSeverity } from '@shepai/core/domain/generated/output';
7
7
  import { useAgentEventsContext } from './agent-events-provider';
8
+ import { useSound } from './use-sound';
8
9
 
9
10
  export interface UseNotificationsResult {
10
11
  requestBrowserPermission: () => Promise<void>;
@@ -20,18 +21,56 @@ const SEVERITY_TO_TOAST: Record<NotificationSeverity, 'success' | 'error' | 'war
20
21
 
21
22
  function dispatchToast(event: NotificationEvent): void {
22
23
  const method = SEVERITY_TO_TOAST[event.severity] ?? 'info';
23
- toast[method](event.featureName, { description: event.message });
24
+ const isActionable = event.eventType === NotificationEventType.WaitingApproval;
25
+ toast[method](event.featureName, {
26
+ description: event.message,
27
+ ...(isActionable && {
28
+ action: {
29
+ label: 'Review',
30
+ onClick: () => {
31
+ window.dispatchEvent(
32
+ new CustomEvent('shep:select-feature', { detail: { featureId: event.featureId } })
33
+ );
34
+ },
35
+ },
36
+ }),
37
+ });
24
38
  }
25
39
 
26
40
  function dispatchBrowserNotification(event: NotificationEvent): void {
27
41
  if (globalThis.Notification?.permission !== 'granted') {
28
42
  return;
29
43
  }
30
- new Notification(event.featureName, { body: event.message });
44
+ new Notification(`Shep: ${event.featureName}`, {
45
+ body: event.message,
46
+ icon: '/favicon-light.svg',
47
+ });
31
48
  }
32
49
 
50
+ const SEVERITY_TO_SOUND = {
51
+ [NotificationSeverity.Success]: 'celebration',
52
+ [NotificationSeverity.Error]: 'caution',
53
+ [NotificationSeverity.Warning]: 'notification',
54
+ [NotificationSeverity.Info]: 'button',
55
+ } as const;
56
+
33
57
  export function useNotifications(): UseNotificationsResult {
34
- const { lastEvent } = useAgentEventsContext();
58
+ const { events } = useAgentEventsContext();
59
+
60
+ const successSound = useSound('celebration', { volume: 0.5 });
61
+ const errorSound = useSound('caution', { volume: 0.5 });
62
+ const warningSound = useSound('notification', { volume: 0.5 });
63
+ const infoSound = useSound('button', { volume: 0.5 });
64
+
65
+ const soundsByName = useMemo<Record<string, { play: () => void }>>(
66
+ () => ({
67
+ celebration: successSound,
68
+ caution: errorSound,
69
+ notification: warningSound,
70
+ button: infoSound,
71
+ }),
72
+ [successSound, errorSound, warningSound, infoSound]
73
+ );
35
74
 
36
75
  const [browserPermissionState, setBrowserPermissionState] = useState<NotificationPermission>(
37
76
  () => {
@@ -40,20 +79,34 @@ export function useNotifications(): UseNotificationsResult {
40
79
  }
41
80
  );
42
81
 
43
- // Track which events we've already dispatched to avoid re-dispatching on re-render
44
- const processedRef = useRef<Set<string>>(new Set());
82
+ // Track how many events from the array we've already processed.
83
+ // Using the array index (instead of lastEvent) prevents React batching
84
+ // from silently dropping events when multiple SSE messages arrive together.
85
+ const processedCountRef = useRef(0);
45
86
 
46
87
  useEffect(() => {
47
- if (!lastEvent) return;
88
+ if (events.length <= processedCountRef.current) return;
48
89
 
49
- // Create a key for deduplication
50
- const key = `${lastEvent.agentRunId}-${lastEvent.eventType}-${lastEvent.timestamp}`;
51
- if (processedRef.current.has(key)) return;
52
- processedRef.current.add(key);
90
+ const newEvents = events.slice(processedCountRef.current);
91
+ processedCountRef.current = events.length;
53
92
 
54
- dispatchToast(lastEvent);
55
- dispatchBrowserNotification(lastEvent);
56
- }, [lastEvent]);
93
+ for (const event of newEvents) {
94
+ // Only notify for actionable events and completion celebrations
95
+ if (
96
+ event.severity !== NotificationSeverity.Error &&
97
+ event.severity !== NotificationSeverity.Warning &&
98
+ event.severity !== NotificationSeverity.Success
99
+ ) {
100
+ continue;
101
+ }
102
+
103
+ dispatchToast(event);
104
+ dispatchBrowserNotification(event);
105
+
106
+ const soundName = SEVERITY_TO_SOUND[event.severity];
107
+ soundsByName[soundName]?.play();
108
+ }
109
+ }, [events, soundsByName]);
57
110
 
58
111
  const requestBrowserPermission = useCallback(async () => {
59
112
  if (typeof globalThis.Notification === 'undefined') return;
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+
5
+ const STORAGE_KEY = 'shep-sound-enabled';
6
+ const SYNC_EVENT = 'shep:sound-toggle';
7
+
8
+ export interface UseSoundEnabledResult {
9
+ enabled: boolean;
10
+ toggle: () => void;
11
+ }
12
+
13
+ export function useSoundEnabled(): UseSoundEnabledResult {
14
+ const [enabled, setEnabled] = useState(true);
15
+
16
+ useEffect(() => {
17
+ const stored = localStorage.getItem(STORAGE_KEY);
18
+ if (stored === 'false') setEnabled(false);
19
+
20
+ // Sync across all useSoundEnabled instances in the same tab
21
+ const onSync = (e: Event) => {
22
+ setEnabled((e as CustomEvent<boolean>).detail);
23
+ };
24
+ window.addEventListener(SYNC_EVENT, onSync);
25
+ return () => window.removeEventListener(SYNC_EVENT, onSync);
26
+ }, []);
27
+
28
+ const toggle = useCallback(() => {
29
+ setEnabled((prev) => {
30
+ const next = !prev;
31
+ localStorage.setItem(STORAGE_KEY, String(next));
32
+ window.dispatchEvent(new CustomEvent(SYNC_EVENT, { detail: next }));
33
+ return next;
34
+ });
35
+ }, []);
36
+
37
+ return { enabled, toggle };
38
+ }
@@ -0,0 +1,109 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Button } from '@/components/ui/button';
4
+ import { useSound, SOUND_NAMES, type SoundName } from './use-sound';
5
+
6
+ /* ------------------------------------------------------------------ */
7
+ /* Helpers */
8
+ /* ------------------------------------------------------------------ */
9
+
10
+ const CATEGORIES: Record<string, SoundName[]> = {
11
+ 'UI Actions': ['button', 'select', 'disabled'],
12
+ Toggles: ['toggle_on', 'toggle_off'],
13
+ Transitions: ['transition_up', 'transition_down'],
14
+ Alerts: ['notification', 'caution', 'celebration'],
15
+ Swipes: ['swipe', 'swipe_01', 'swipe_02', 'swipe_03', 'swipe_04', 'swipe_05'],
16
+ Taps: ['tap_01', 'tap_02', 'tap_03', 'tap_04', 'tap_05'],
17
+ Typing: ['type_01', 'type_02', 'type_03', 'type_04', 'type_05'],
18
+ Loops: ['progress_loop', 'ringtone_loop'],
19
+ };
20
+
21
+ /* ------------------------------------------------------------------ */
22
+ /* Single sound player (used per button) */
23
+ /* ------------------------------------------------------------------ */
24
+
25
+ function SoundButton({ name, volume }: { name: SoundName; volume: number }) {
26
+ const isLoop = name.includes('loop');
27
+ const { play, stop } = useSound(name, { volume, loop: isLoop });
28
+ const [playing, setPlaying] = useState(false);
29
+
30
+ const handleClick = () => {
31
+ if (isLoop && playing) {
32
+ stop();
33
+ setPlaying(false);
34
+ } else {
35
+ play();
36
+ setPlaying(true);
37
+ if (!isLoop) {
38
+ setTimeout(() => setPlaying(false), 2000);
39
+ }
40
+ }
41
+ };
42
+
43
+ return (
44
+ <Button
45
+ size="sm"
46
+ variant={playing ? 'default' : 'outline'}
47
+ onClick={handleClick}
48
+ className="min-w-[120px] font-mono text-xs"
49
+ >
50
+ {isLoop && playing ? `Stop ${name}` : name}
51
+ </Button>
52
+ );
53
+ }
54
+
55
+ /* ------------------------------------------------------------------ */
56
+ /* Full catalog demo */
57
+ /* ------------------------------------------------------------------ */
58
+
59
+ function SoundCatalog({ volume }: { volume: number }) {
60
+ return (
61
+ <div className="flex w-[560px] flex-col gap-6">
62
+ <div className="flex flex-col gap-1">
63
+ <h2 className="text-base font-semibold">Sound Library</h2>
64
+ <p className="text-muted-foreground text-sm">
65
+ {SOUND_NAMES.length} sounds available at{' '}
66
+ <code className="bg-muted rounded px-1 py-0.5 text-xs">/sounds/*.wav</code>
67
+ </p>
68
+ </div>
69
+
70
+ {Object.entries(CATEGORIES).map(([category, sounds]) => (
71
+ <div key={category} className="flex flex-col gap-2">
72
+ <h3 className="text-sm font-semibold">{category}</h3>
73
+ <div className="flex flex-wrap gap-2">
74
+ {sounds.map((name) => (
75
+ <SoundButton key={name} name={name} volume={volume} />
76
+ ))}
77
+ </div>
78
+ </div>
79
+ ))}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ /* ------------------------------------------------------------------ */
85
+ /* Storybook meta */
86
+ /* ------------------------------------------------------------------ */
87
+
88
+ const meta: Meta = {
89
+ title: 'Hooks/useSound',
90
+ parameters: { layout: 'centered' },
91
+ tags: ['autodocs'],
92
+ argTypes: {
93
+ volume: {
94
+ control: { type: 'range', min: 0, max: 1, step: 0.1 },
95
+ description: 'Playback volume (0 to 1)',
96
+ defaultValue: 0.5,
97
+ },
98
+ },
99
+ args: {
100
+ volume: 0.5,
101
+ },
102
+ };
103
+
104
+ export default meta;
105
+ type Story = StoryObj<{ volume: number }>;
106
+
107
+ export const Catalog: Story = {
108
+ render: ({ volume }) => <SoundCatalog volume={volume} />,
109
+ };
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef } from 'react';
4
+ import { useSoundEnabled } from './use-sound-enabled';
5
+
6
+ const SOUND_NAMES = [
7
+ 'button',
8
+ 'caution',
9
+ 'celebration',
10
+ 'disabled',
11
+ 'notification',
12
+ 'progress_loop',
13
+ 'ringtone_loop',
14
+ 'select',
15
+ 'swipe',
16
+ 'swipe_01',
17
+ 'swipe_02',
18
+ 'swipe_03',
19
+ 'swipe_04',
20
+ 'swipe_05',
21
+ 'tap_01',
22
+ 'tap_02',
23
+ 'tap_03',
24
+ 'tap_04',
25
+ 'tap_05',
26
+ 'toggle_off',
27
+ 'toggle_on',
28
+ 'transition_down',
29
+ 'transition_up',
30
+ 'type_01',
31
+ 'type_02',
32
+ 'type_03',
33
+ 'type_04',
34
+ 'type_05',
35
+ ] as const;
36
+
37
+ export type SoundName = (typeof SOUND_NAMES)[number];
38
+
39
+ export interface UseSoundOptions {
40
+ volume?: number;
41
+ loop?: boolean;
42
+ }
43
+
44
+ export interface UseSoundResult {
45
+ play: () => void;
46
+ stop: () => void;
47
+ isPlaying: boolean;
48
+ }
49
+
50
+ export function useSound(name: SoundName, options: UseSoundOptions = {}): UseSoundResult {
51
+ const { volume = 1, loop = false } = options;
52
+ const { enabled } = useSoundEnabled();
53
+ const audioRef = useRef<HTMLAudioElement | null>(null);
54
+ const isPlayingRef = useRef(false);
55
+
56
+ useEffect(() => {
57
+ if (typeof window === 'undefined') return;
58
+ const audio = new Audio(`/sounds/${name}.wav`);
59
+ audio.preload = 'auto';
60
+ audio.loop = loop;
61
+ audio.volume = Math.max(0, Math.min(1, volume));
62
+ audio.addEventListener('ended', () => {
63
+ isPlayingRef.current = false;
64
+ });
65
+ audioRef.current = audio;
66
+
67
+ return () => {
68
+ audio.pause();
69
+ audio.removeAttribute('src');
70
+ audioRef.current = null;
71
+ isPlayingRef.current = false;
72
+ };
73
+ }, [name, loop, volume]);
74
+
75
+ const play = useCallback(() => {
76
+ if (!enabled) return;
77
+ const audio = audioRef.current;
78
+ if (!audio) return;
79
+ audio.currentTime = 0;
80
+ audio.play()?.catch((err) => {
81
+ if (process.env.NODE_ENV === 'development') {
82
+ // eslint-disable-next-line no-console
83
+ console.warn(`[useSound] play("${name}") blocked:`, err.message);
84
+ }
85
+ });
86
+ isPlayingRef.current = true;
87
+ }, [enabled, name]);
88
+
89
+ const stop = useCallback(() => {
90
+ const audio = audioRef.current;
91
+ if (!audio) return;
92
+ audio.pause();
93
+ audio.currentTime = 0;
94
+ isPlayingRef.current = false;
95
+ }, []);
96
+
97
+ return { play, stop, isPlaying: isPlayingRef.current };
98
+ }
99
+
100
+ /** All available sound names for enumeration (e.g. in Storybook). */
101
+ export { SOUND_NAMES };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Service Worker: agent-events-sw.js
3
+ *
4
+ * Maintains a single SSE connection to /api/agent-events and broadcasts
5
+ * NotificationEvent deltas to all connected tabs via postMessage.
6
+ *
7
+ * Messages FROM clients:
8
+ * { type: 'subscribe', runId?: string } — register this tab
9
+ * { type: 'unsubscribe' } — unregister this tab
10
+ *
11
+ * Messages TO clients:
12
+ * { type: 'notification', data: NotificationEvent }
13
+ * { type: 'status', status: 'connected' | 'connecting' | 'disconnected' }
14
+ */
15
+
16
+ /* global self, EventSource, setTimeout, clearTimeout */
17
+
18
+ const BASE_BACKOFF_MS = 1000;
19
+ const MAX_BACKOFF_MS = 30_000;
20
+ const STABLE_CONNECTION_MS = 5_000;
21
+
22
+ /** @type {EventSource | null} */
23
+ let eventSource = null;
24
+ /** @type {string | undefined} */
25
+ let currentRunId;
26
+ let backoff = BASE_BACKOFF_MS;
27
+ /** @type {ReturnType<typeof setTimeout> | null} */
28
+ let reconnectTimer = null;
29
+ /** @type {ReturnType<typeof setTimeout> | null} */
30
+ let stableTimer = null;
31
+ /** @type {'connected' | 'connecting' | 'disconnected'} */
32
+ let connectionStatus = 'disconnected';
33
+ let subscriberCount = 0;
34
+
35
+ // --- Lifecycle ---
36
+
37
+ self.addEventListener('install', () => {
38
+ self.skipWaiting();
39
+ });
40
+
41
+ self.addEventListener('activate', (event) => {
42
+ event.waitUntil(
43
+ self.clients.claim().then(async () => {
44
+ // After claiming clients, check if there are open tabs that need a connection.
45
+ // This handles the case where the SW updated via skipWaiting() and the old
46
+ // SW's EventSource was closed — we auto-start for existing clients.
47
+ const clients = await self.clients.matchAll({ type: 'window' });
48
+ if (clients.length > 0 && subscriberCount === 0) {
49
+ subscriberCount = clients.length;
50
+ connect();
51
+ }
52
+ })
53
+ );
54
+ });
55
+
56
+ // --- Client messaging ---
57
+
58
+ self.addEventListener('message', (event) => {
59
+ const { type, runId } = event.data ?? {};
60
+
61
+ if (type === 'subscribe') {
62
+ subscriberCount++;
63
+ currentRunId = runId;
64
+
65
+ // Send current status to the new subscriber immediately
66
+ event.source?.postMessage({ type: 'status', status: connectionStatus });
67
+
68
+ // Start connection if this is the first subscriber
69
+ if (subscriberCount === 1) {
70
+ connect();
71
+ }
72
+ } else if (type === 'unsubscribe') {
73
+ subscriberCount = Math.max(0, subscriberCount - 1);
74
+
75
+ // Close connection when no subscribers remain
76
+ if (subscriberCount === 0) {
77
+ disconnect();
78
+ }
79
+ }
80
+ });
81
+
82
+ // --- SSE connection management ---
83
+
84
+ function connect() {
85
+ if (eventSource) return;
86
+
87
+ const url = currentRunId
88
+ ? `/api/agent-events?runId=${encodeURIComponent(currentRunId)}`
89
+ : '/api/agent-events';
90
+
91
+ eventSource = new EventSource(url);
92
+ setStatus('connecting');
93
+
94
+ eventSource.onopen = () => {
95
+ setStatus('connected');
96
+ // Only reset backoff after connection is stable
97
+ stableTimer = setTimeout(() => {
98
+ stableTimer = null;
99
+ backoff = BASE_BACKOFF_MS;
100
+ }, STABLE_CONNECTION_MS);
101
+ };
102
+
103
+ eventSource.onerror = () => {
104
+ cleanup();
105
+ setStatus('disconnected');
106
+
107
+ // Don't reconnect if no subscribers
108
+ if (subscriberCount <= 0) return;
109
+
110
+ const delay = backoff;
111
+ backoff = Math.min(delay * 2, MAX_BACKOFF_MS);
112
+
113
+ reconnectTimer = setTimeout(() => {
114
+ reconnectTimer = null;
115
+ connect();
116
+ }, delay);
117
+ };
118
+
119
+ eventSource.addEventListener('notification', (event) => {
120
+ /** @type {unknown} */
121
+ let data;
122
+ try {
123
+ data = JSON.parse(event.data);
124
+ } catch {
125
+ return;
126
+ }
127
+ broadcast({ type: 'notification', data });
128
+ });
129
+ }
130
+
131
+ function disconnect() {
132
+ if (reconnectTimer !== null) {
133
+ clearTimeout(reconnectTimer);
134
+ reconnectTimer = null;
135
+ }
136
+ cleanup();
137
+ backoff = BASE_BACKOFF_MS;
138
+ setStatus('disconnected');
139
+ }
140
+
141
+ function cleanup() {
142
+ if (stableTimer !== null) {
143
+ clearTimeout(stableTimer);
144
+ stableTimer = null;
145
+ }
146
+ if (eventSource) {
147
+ eventSource.close();
148
+ eventSource = null;
149
+ }
150
+ }
151
+
152
+ // --- Helpers ---
153
+
154
+ function setStatus(status) {
155
+ connectionStatus = status;
156
+ broadcast({ type: 'status', status });
157
+ }
158
+
159
+ async function broadcast(message) {
160
+ const clients = await self.clients.matchAll({ type: 'window' });
161
+ for (const client of clients) {
162
+ client.postMessage(message);
163
+ }
164
+ }