@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.
- package/README.md +22 -8
- package/apis/json-schema/NotificationEvent.yaml +4 -0
- package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts +49 -0
- package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts.map +1 -0
- package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.js +7 -0
- package/dist/packages/core/src/application/ports/output/services/index.d.ts +1 -0
- package/dist/packages/core/src/application/ports/output/services/index.d.ts.map +1 -1
- package/dist/packages/core/src/domain/generated/output.d.ts +4 -0
- package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/di/container.js +2 -0
- package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts +1 -0
- package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/browser-opener.service.js +4 -0
- package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts +16 -0
- package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.js +72 -0
- package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts +7 -0
- package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.js +9 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts +4 -0
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/notifications/notification-bus.js +19 -6
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts +5 -3
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/notifications/notification-watcher.service.js +21 -13
- package/dist/src/presentation/cli/commands/_serve.command.d.ts +32 -0
- package/dist/src/presentation/cli/commands/_serve.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/_serve.command.js +91 -0
- package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts +27 -0
- package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/daemon/start-daemon.js +135 -0
- package/dist/src/presentation/cli/commands/start.command.d.ts +15 -0
- package/dist/src/presentation/cli/commands/start.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/start.command.js +33 -0
- package/dist/src/presentation/cli/commands/status.command.d.ts +20 -0
- package/dist/src/presentation/cli/commands/status.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/status.command.js +119 -0
- package/dist/src/presentation/cli/commands/stop.command.d.ts +22 -0
- package/dist/src/presentation/cli/commands/stop.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/stop.command.js +79 -0
- package/dist/src/presentation/cli/commands/ui.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/ui.command.js +2 -1
- package/dist/src/presentation/cli/index.d.ts +5 -2
- package/dist/src/presentation/cli/index.d.ts.map +1 -1
- package/dist/src/presentation/cli/index.js +23 -4
- package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts +13 -0
- package/dist/src/presentation/web/app/api/agent-events/health/route.d.ts.map +1 -0
- package/dist/src/presentation/web/app/api/agent-events/health/route.js +60 -0
- package/dist/src/presentation/web/app/api/agent-events/route.d.ts +8 -4
- package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -1
- package/dist/src/presentation/web/app/api/agent-events/route.js +227 -55
- package/dist/src/presentation/web/app/layout.d.ts.map +1 -1
- package/dist/src/presentation/web/app/layout.js +14 -0
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts +5 -1
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +29 -1
- package/dist/src/presentation/web/components/common/repository-node/repository-node.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/repository-node/repository-node.js +1 -1
- package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts +2 -0
- package/dist/src/presentation/web/components/common/sound-toggle/index.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/index.js +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts +2 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.js +9 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts +8 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.d.ts.map +1 -0
- package/dist/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.js +17 -0
- package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +14 -2
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +1 -0
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +151 -19
- package/dist/src/presentation/web/components/layouts/app-shell/app-shell.d.ts.map +1 -1
- package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +2 -1
- package/dist/src/presentation/web/dev-server.js +14 -0
- package/dist/src/presentation/web/hooks/use-agent-events.d.ts +7 -0
- package/dist/src/presentation/web/hooks/use-agent-events.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-agent-events.js +135 -47
- package/dist/src/presentation/web/hooks/use-notifications.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-notifications.js +56 -16
- package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts.map +1 -1
- package/dist/src/presentation/web/hooks/use-notifications.stories.js +28 -9
- package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts +6 -0
- package/dist/src/presentation/web/hooks/use-sound-enabled.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-sound-enabled.js +27 -0
- package/dist/src/presentation/web/hooks/use-sound.d.ts +15 -0
- package/dist/src/presentation/web/hooks/use-sound.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-sound.js +83 -0
- package/dist/src/presentation/web/hooks/use-sound.stories.d.ts +8 -0
- package/dist/src/presentation/web/hooks/use-sound.stories.d.ts.map +1 -0
- package/dist/src/presentation/web/hooks/use-sound.stories.js +67 -0
- package/dist/src/presentation/web/public/agent-events-sw.d.ts +35 -0
- package/dist/src/presentation/web/public/agent-events-sw.d.ts.map +1 -0
- package/dist/src/presentation/web/public/agent-events-sw.js +140 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-path-routes-manifest.json +1 -0
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/routes-manifest.json +6 -0
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
- package/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
- package/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
- package/web/.next/server/app/api/agent-events/health/route.js +6 -0
- package/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
- package/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
- package/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
- package/web/.next/server/app/api/agent-events/route.js +2 -1
- package/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
- package/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page.js +1 -1
- package/web/.next/server/app/version/page.js.nft.json +1 -1
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app-paths-manifest.json +1 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -0
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js +3 -0
- package/web/.next/server/chunks/744ca_web__next-internal_server_app_api_agent-events_health_route_actions_4d471160.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
- package/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js.map +1 -0
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
- 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
- package/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
- package/web/.next/server/chunks/ssr/_1632a21f._.js.map +1 -0
- package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/app-path-routes-manifest.json +1 -0
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/routes-manifest.json +6 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/build-manifest.json +11 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js +6 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.map +5 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route.js.nft.json +1 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/health/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js +2 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js.nft.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app-paths-manifest.json +1 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +6 -0
- 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
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__3b22a1eb._.js +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__d9d410a8._.js +3 -0
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- 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
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{_1fafa9db._.js → _1632a21f._.js} +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_67178b88._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/app/api/agent-events/health/route.ts +69 -0
- package/web/.next/standalone/src/presentation/web/app/api/agent-events/route.ts +258 -54
- package/web/.next/standalone/src/presentation/web/app/layout.tsx +14 -0
- package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +39 -2
- package/web/.next/standalone/src/presentation/web/components/common/repository-node/repository-node.tsx +1 -0
- package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/index.ts +1 -0
- package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.stories.tsx +27 -0
- package/web/.next/standalone/src/presentation/web/components/common/sound-toggle/sound-toggle.tsx +21 -0
- package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +15 -1
- package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +194 -31
- package/web/.next/standalone/src/presentation/web/components/layouts/app-shell/app-shell.tsx +3 -1
- package/web/.next/standalone/src/presentation/web/dev-server.ts +21 -0
- package/web/.next/standalone/src/presentation/web/hooks/use-agent-events.ts +161 -52
- package/web/.next/standalone/src/presentation/web/hooks/use-notifications.stories.tsx +39 -10
- package/web/.next/standalone/src/presentation/web/hooks/use-notifications.ts +68 -15
- package/web/.next/standalone/src/presentation/web/hooks/use-sound-enabled.ts +38 -0
- package/web/.next/standalone/src/presentation/web/hooks/use-sound.stories.tsx +109 -0
- package/web/.next/standalone/src/presentation/web/hooks/use-sound.ts +101 -0
- package/web/.next/standalone/src/presentation/web/public/agent-events-sw.js +164 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/button.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/caution.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/celebration.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/disabled.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/notification.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/progress_loop.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/ringtone_loop.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/select.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_01.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_02.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_03.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_04.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/swipe_05.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_01.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_02.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_03.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_04.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/tap_05.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/toggle_off.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/toggle_on.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/transition_down.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/transition_up.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_01.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_02.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_03.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_04.wav +0 -0
- package/web/.next/standalone/src/presentation/web/public/sounds/type_05.wav +0 -0
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/static/chunks/1f13bc20e938335c.css +2 -0
- package/web/.next/static/chunks/3eaa6f2966e27b8f.js +10 -0
- package/web/.next/static/chunks/{68fb91b6eca14536.js → 8f4461aee9a05741.js} +2 -2
- package/web/.next/static/chunks/9a82d8c76f175235.js +1 -0
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- package/web/.next/types/link.d.ts +1 -0
- package/web/.next/types/routes.d.ts +2 -1
- package/web/.next/types/validator.ts +9 -0
- package/web/public/agent-events-sw.js +164 -0
- package/web/public/sounds/button.wav +0 -0
- package/web/public/sounds/caution.wav +0 -0
- package/web/public/sounds/celebration.wav +0 -0
- package/web/public/sounds/disabled.wav +0 -0
- package/web/public/sounds/notification.wav +0 -0
- package/web/public/sounds/progress_loop.wav +0 -0
- package/web/public/sounds/ringtone_loop.wav +0 -0
- package/web/public/sounds/select.wav +0 -0
- package/web/public/sounds/swipe.wav +0 -0
- package/web/public/sounds/swipe_01.wav +0 -0
- package/web/public/sounds/swipe_02.wav +0 -0
- package/web/public/sounds/swipe_03.wav +0 -0
- package/web/public/sounds/swipe_04.wav +0 -0
- package/web/public/sounds/swipe_05.wav +0 -0
- package/web/public/sounds/tap_01.wav +0 -0
- package/web/public/sounds/tap_02.wav +0 -0
- package/web/public/sounds/tap_03.wav +0 -0
- package/web/public/sounds/tap_04.wav +0 -0
- package/web/public/sounds/tap_05.wav +0 -0
- package/web/public/sounds/toggle_off.wav +0 -0
- package/web/public/sounds/toggle_on.wav +0 -0
- package/web/public/sounds/transition_down.wav +0 -0
- package/web/public/sounds/transition_up.wav +0 -0
- package/web/public/sounds/type_01.wav +0 -0
- package/web/public/sounds/type_02.wav +0 -0
- package/web/public/sounds/type_03.wav +0 -0
- package/web/public/sounds/type_04.wav +0 -0
- package/web/public/sounds/type_05.wav +0 -0
- package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
- package/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js.map +0 -1
- package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +0 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/[root-of-the-server]__7d5c8b74._.js +0 -6
- package/web/.next/static/chunks/13d5334b17480a16.js +0 -10
- package/web/.next/static/chunks/16dec8021e251230.css +0 -2
- package/web/.next/static/chunks/22f30ce91a3c79c1.js +0 -1
- /package/web/.next/server/chunks/ssr/{[root-of-the-server]__249c74f6._.js.map → [root-of-the-server]__6e8b5181._.js.map} +0 -0
- /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → JmDtIt3S6EfLuRKwlzJrW}/_buildManifest.js +0 -0
- /package/web/.next/static/{nFvWyY2BF3AIS3FpZBKEb → JmDtIt3S6EfLuRKwlzJrW}/_clientMiddlewareManifest.json +0 -0
- /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
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
44
|
-
|
|
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 (
|
|
88
|
+
if (events.length <= processedCountRef.current) return;
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
Binary file
|