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