@shepai/cli 1.53.1 → 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/json-schema/NotificationEvent.yaml +4 -0
- 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/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/ui.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/ui.command.js +2 -1
- 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/features/features-canvas/features-canvas.js +1 -1
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts +1 -0
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts.map +1 -1
- package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.js +31 -1
- 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/{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/features/features-canvas/features-canvas.stories.tsx +41 -0
- package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +1 -1
- 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/702384d364482e4b.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/212e3c682d3b7f50.js +0 -10
- package/web/.next/static/chunks/22f30ce91a3c79c1.js +0 -1
- package/web/.next/static/chunks/476fe030d242c153.css +0 -2
- /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/{EWkET8BUeCcZJxcYQpxDY → RwzxYU9D2fPc_4j5RYu0v}/_buildManifest.js +0 -0
- /package/web/.next/static/{EWkET8BUeCcZJxcYQpxDY → RwzxYU9D2fPc_4j5RYu0v}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{EWkET8BUeCcZJxcYQpxDY → RwzxYU9D2fPc_4j5RYu0v}/_ssgManifest.js +0 -0
package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js
CHANGED
|
@@ -9,6 +9,8 @@ import { deleteFeature } from '../../../app/actions/delete-feature.js';
|
|
|
9
9
|
import { addRepository } from '../../../app/actions/add-repository.js';
|
|
10
10
|
import { deleteRepository } from '../../../app/actions/delete-repository.js';
|
|
11
11
|
import { useAgentEventsContext } from '../../../hooks/agent-events-provider.js';
|
|
12
|
+
import { useSound } from '../../../hooks/use-sound.js';
|
|
13
|
+
import { mapEventTypeToState, mapPhaseNameToLifecycle, } from '../../common/feature-node/derive-feature-state.js';
|
|
12
14
|
let nextFeatureId = 0;
|
|
13
15
|
export function useControlCenterState(initialNodes, initialEdges) {
|
|
14
16
|
const router = useRouter();
|
|
@@ -28,6 +30,9 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
28
30
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
29
31
|
const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false);
|
|
30
32
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
33
|
+
const deleteSound = useSound('transition_down', { volume: 0.5 });
|
|
34
|
+
const createSound = useSound('transition_up', { volume: 0.5 });
|
|
35
|
+
const clickSound = useSound('tap_01', { volume: 0.3 });
|
|
31
36
|
const [pendingRepoNodeId, setPendingRepoNodeId] = useState(null);
|
|
32
37
|
// Sync server props into local state when router.refresh() delivers new data
|
|
33
38
|
const initialNodeKey = initialNodes
|
|
@@ -38,6 +43,17 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
38
43
|
.map((e) => e.id)
|
|
39
44
|
.sort()
|
|
40
45
|
.join(',');
|
|
46
|
+
// Track previous feature states so we can detect transitions on server refresh
|
|
47
|
+
const prevFeatureStatesRef = useRef(new Map());
|
|
48
|
+
// Stable key that changes when feature DATA changes (state/lifecycle), not just IDs.
|
|
49
|
+
const initialDataKey = initialNodes
|
|
50
|
+
.filter((n) => n.type === 'featureNode')
|
|
51
|
+
.map((n) => {
|
|
52
|
+
const d = n.data;
|
|
53
|
+
return `${n.id}:${d.state}:${d.lifecycle}`;
|
|
54
|
+
})
|
|
55
|
+
.sort()
|
|
56
|
+
.join(',');
|
|
41
57
|
// Sync server props into local state — keyed by derived strings (initialNodeKey/initialEdgeKey)
|
|
42
58
|
// to avoid infinite re-renders from unstable array references.
|
|
43
59
|
useEffect(() => {
|
|
@@ -69,33 +85,128 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
69
85
|
useEffect(() => {
|
|
70
86
|
setEdges(initialEdges);
|
|
71
87
|
}, [initialEdgeKey, initialEdges, setEdges]);
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
// Fallback notifications from server-refresh state transitions.
|
|
89
|
+
// Fires only when SSE didn't already deliver the matching event for a feature,
|
|
90
|
+
// e.g. when the SSE connection was down or the event was seeded into the cache.
|
|
91
|
+
const { events } = useAgentEventsContext();
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const prevStates = prevFeatureStatesRef.current;
|
|
94
|
+
for (const node of initialNodes) {
|
|
95
|
+
if (node.type !== 'featureNode')
|
|
96
|
+
continue;
|
|
97
|
+
const data = node.data;
|
|
98
|
+
const prev = prevStates.get(node.id);
|
|
99
|
+
if (prev !== undefined && prev !== data.state) {
|
|
100
|
+
// Check if SSE already delivered a matching event for this feature
|
|
101
|
+
const sseAlreadyCovered = events.some((e) => e.featureId === data.featureId && mapEventTypeToState(e.eventType) === data.state);
|
|
102
|
+
if (!sseAlreadyCovered) {
|
|
103
|
+
if (data.state === 'done') {
|
|
104
|
+
toast.success(data.name, { description: 'Feature completed!' });
|
|
105
|
+
}
|
|
106
|
+
else if (data.state === 'action-required') {
|
|
107
|
+
toast.warning(data.name, {
|
|
108
|
+
description: 'Waiting for your approval',
|
|
109
|
+
action: {
|
|
110
|
+
label: 'Review',
|
|
111
|
+
onClick: () => {
|
|
112
|
+
window.dispatchEvent(new CustomEvent('shep:select-feature', {
|
|
113
|
+
detail: { featureId: data.featureId },
|
|
114
|
+
}));
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (data.state === 'error') {
|
|
120
|
+
toast.error(data.name, { description: data.errorMessage ?? 'Agent failed' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
prevStates.set(node.id, data.state);
|
|
125
|
+
}
|
|
126
|
+
}, [initialDataKey, initialNodes, events]);
|
|
127
|
+
// Targeted optimistic updates from SSE agent events + debounced reconciliation.
|
|
128
|
+
// Uses `events` array (not `lastEvent`) so that React batching cannot silently
|
|
129
|
+
// drop events when multiple SSE messages arrive in the same tick.
|
|
130
|
+
const processedEventCountRef = useRef(0);
|
|
131
|
+
const reconcileTimerRef = useRef(undefined);
|
|
75
132
|
useEffect(() => {
|
|
76
|
-
|
|
133
|
+
return () => clearTimeout(reconcileTimerRef.current);
|
|
134
|
+
}, []);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (events.length <= processedEventCountRef.current)
|
|
77
137
|
return;
|
|
78
|
-
const
|
|
79
|
-
|
|
138
|
+
const newEvents = events.slice(processedEventCountRef.current);
|
|
139
|
+
processedEventCountRef.current = events.length;
|
|
140
|
+
for (const event of newEvents) {
|
|
141
|
+
const newState = mapEventTypeToState(event.eventType);
|
|
142
|
+
const newLifecycle = mapPhaseNameToLifecycle(event.phaseName);
|
|
143
|
+
// Targeted node update — only clone the matched node
|
|
144
|
+
setNodes((prev) => prev.map((node) => {
|
|
145
|
+
if (node.type !== 'featureNode')
|
|
146
|
+
return node;
|
|
147
|
+
const data = node.data;
|
|
148
|
+
if (data.featureId !== event.featureId)
|
|
149
|
+
return node;
|
|
150
|
+
return {
|
|
151
|
+
...node,
|
|
152
|
+
data: {
|
|
153
|
+
...data,
|
|
154
|
+
state: newState,
|
|
155
|
+
...(newLifecycle !== undefined && { lifecycle: newLifecycle }),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}));
|
|
159
|
+
// Update drawer if it's showing the matched feature
|
|
160
|
+
setSelectedNode((prev) => {
|
|
161
|
+
if (prev?.featureId !== event.featureId)
|
|
162
|
+
return prev;
|
|
163
|
+
return {
|
|
164
|
+
...prev,
|
|
165
|
+
state: newState,
|
|
166
|
+
...(newLifecycle !== undefined && { lifecycle: newLifecycle }),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Debounced background reconciliation (3s after last SSE event)
|
|
171
|
+
clearTimeout(reconcileTimerRef.current);
|
|
172
|
+
reconcileTimerRef.current = setTimeout(() => router.refresh(), 3000);
|
|
173
|
+
}, [events, router]);
|
|
174
|
+
// Periodic polling fallback: refresh server data every 5s when any feature
|
|
175
|
+
// is in an active state (running/action-required). This ensures the UI stays
|
|
176
|
+
// current even if the SSE connection drops — belt-and-suspenders alongside SSE.
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const hasActiveFeature = nodes.some((n) => {
|
|
179
|
+
if (n.type !== 'featureNode')
|
|
180
|
+
return false;
|
|
181
|
+
const data = n.data;
|
|
182
|
+
return (data.state === 'running' || data.state === 'action-required' || data.state === 'creating');
|
|
183
|
+
});
|
|
184
|
+
if (!hasActiveFeature)
|
|
80
185
|
return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}, [
|
|
186
|
+
const interval = setInterval(() => router.refresh(), 5_000);
|
|
187
|
+
return () => clearInterval(interval);
|
|
188
|
+
}, [nodes, router]);
|
|
84
189
|
const onNodesChange = useCallback((changes) => {
|
|
85
190
|
setNodes((ns) => applyNodeChanges(changes, ns));
|
|
86
191
|
}, []);
|
|
192
|
+
const closeSound = useSound('transition_down', { volume: 0.01 });
|
|
87
193
|
const clearSelection = useCallback(() => {
|
|
88
|
-
setSelectedNode(
|
|
89
|
-
|
|
194
|
+
setSelectedNode((prev) => {
|
|
195
|
+
if (prev)
|
|
196
|
+
closeSound.play();
|
|
197
|
+
return null;
|
|
198
|
+
});
|
|
199
|
+
}, [closeSound]);
|
|
90
200
|
const handleNodeClick = useCallback((_event, node) => {
|
|
91
201
|
if (node.type === 'featureNode') {
|
|
92
202
|
const data = node.data;
|
|
93
203
|
if (data.state === 'creating')
|
|
94
204
|
return;
|
|
205
|
+
clickSound.play();
|
|
95
206
|
setIsCreateDrawerOpen(false);
|
|
96
207
|
setSelectedNode(data);
|
|
97
208
|
}
|
|
98
|
-
}, []);
|
|
209
|
+
}, [clickSound]);
|
|
99
210
|
// Keyboard shortcut: Escape to deselect
|
|
100
211
|
useEffect(() => {
|
|
101
212
|
const handleKeyDown = (e) => {
|
|
@@ -242,9 +353,10 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
242
353
|
return id;
|
|
243
354
|
}, [setEdges]);
|
|
244
355
|
const handleAddFeature = useCallback(() => {
|
|
356
|
+
clickSound.play();
|
|
245
357
|
setSelectedNode(null);
|
|
246
358
|
setIsCreateDrawerOpen(true);
|
|
247
|
-
}, []);
|
|
359
|
+
}, [clickSound]);
|
|
248
360
|
const [pendingParentFeatureId, setPendingParentFeatureId] = useState();
|
|
249
361
|
const handleCreateFeatureSubmit = useCallback((data) => {
|
|
250
362
|
// 1. Insert optimistic node instantly
|
|
@@ -272,6 +384,7 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
272
384
|
toast.error(result.error);
|
|
273
385
|
return;
|
|
274
386
|
}
|
|
387
|
+
createSound.play();
|
|
275
388
|
router.refresh();
|
|
276
389
|
})
|
|
277
390
|
.catch(() => {
|
|
@@ -280,11 +393,12 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
280
393
|
setEdges((prev) => prev.filter((e) => e.target !== tempId));
|
|
281
394
|
toast.error('Failed to create feature');
|
|
282
395
|
});
|
|
283
|
-
}, [router, createFeatureNode, pendingRepoNodeId, pendingParentFeatureId, setEdges]);
|
|
396
|
+
}, [router, createFeatureNode, pendingRepoNodeId, createSound, pendingParentFeatureId, setEdges]);
|
|
284
397
|
const closeCreateDrawer = useCallback(() => {
|
|
398
|
+
closeSound.play();
|
|
285
399
|
setIsCreateDrawerOpen(false);
|
|
286
400
|
setPendingParentFeatureId(undefined);
|
|
287
|
-
}, []);
|
|
401
|
+
}, [closeSound]);
|
|
288
402
|
const handleDeleteFeature = useCallback(async (featureId) => {
|
|
289
403
|
setIsDeleting(true);
|
|
290
404
|
try {
|
|
@@ -305,6 +419,7 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
305
419
|
setEdges(result.edges);
|
|
306
420
|
return result.nodes;
|
|
307
421
|
});
|
|
422
|
+
deleteSound.play();
|
|
308
423
|
toast.success('Feature deleted successfully');
|
|
309
424
|
router.refresh();
|
|
310
425
|
}
|
|
@@ -314,7 +429,7 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
314
429
|
finally {
|
|
315
430
|
setIsDeleting(false);
|
|
316
431
|
}
|
|
317
|
-
}, [router, edges, setEdges]);
|
|
432
|
+
}, [router, edges, deleteSound, setEdges]);
|
|
318
433
|
const handleDeleteRepository = useCallback(async (repositoryId) => {
|
|
319
434
|
const repoNodeId = `repo-${repositoryId}`;
|
|
320
435
|
// Optimistic: remove node and its edges immediately
|
|
@@ -327,6 +442,7 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
327
442
|
router.refresh();
|
|
328
443
|
return;
|
|
329
444
|
}
|
|
445
|
+
deleteSound.play();
|
|
330
446
|
toast.success('Repository removed');
|
|
331
447
|
router.refresh();
|
|
332
448
|
}
|
|
@@ -334,12 +450,13 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
334
450
|
toast.error('Failed to remove repository');
|
|
335
451
|
router.refresh();
|
|
336
452
|
}
|
|
337
|
-
}, [router, setEdges]);
|
|
453
|
+
}, [router, deleteSound, setEdges]);
|
|
338
454
|
const handleAddFeatureToRepo = useCallback((repoNodeId) => {
|
|
455
|
+
clickSound.play();
|
|
339
456
|
setSelectedNode(null);
|
|
340
457
|
setPendingRepoNodeId(repoNodeId);
|
|
341
458
|
setIsCreateDrawerOpen(true);
|
|
342
|
-
}, []);
|
|
459
|
+
}, [clickSound]);
|
|
343
460
|
const handleAddFeatureToFeature = useCallback((featureNodeId) => {
|
|
344
461
|
// Extract feature ID from node ID (format: "feat-<uuid>")
|
|
345
462
|
const featureId = featureNodeId.startsWith('feat-') ? featureNodeId.slice(5) : featureNodeId;
|
|
@@ -428,6 +545,7 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
428
545
|
target: e.target === tempId ? realId : e.target,
|
|
429
546
|
id: e.id.replace(tempId, realId),
|
|
430
547
|
})));
|
|
548
|
+
createSound.play();
|
|
431
549
|
router.refresh();
|
|
432
550
|
})
|
|
433
551
|
.catch(() => {
|
|
@@ -439,7 +557,20 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
439
557
|
: n));
|
|
440
558
|
toast.error('Failed to add repository');
|
|
441
559
|
});
|
|
442
|
-
}, [router, setEdges]);
|
|
560
|
+
}, [router, createSound, setEdges]);
|
|
561
|
+
// Ref keeps latest nodes so selectFeatureById has a stable identity
|
|
562
|
+
const nodesRef = useRef(nodes);
|
|
563
|
+
nodesRef.current = nodes;
|
|
564
|
+
const selectFeatureById = useCallback((featureId) => {
|
|
565
|
+
const node = nodesRef.current.find((n) => n.type === 'featureNode' && n.data.featureId === featureId);
|
|
566
|
+
if (node) {
|
|
567
|
+
const data = node.data;
|
|
568
|
+
if (data.state !== 'creating') {
|
|
569
|
+
setIsCreateDrawerOpen(false);
|
|
570
|
+
setSelectedNode(data);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}, []);
|
|
443
574
|
const pendingNode = pendingRepoNodeId ? nodes.find((n) => n.id === pendingRepoNodeId) : null;
|
|
444
575
|
const pendingRepositoryPath = pendingNode?.data?.repositoryPath ?? '';
|
|
445
576
|
return {
|
|
@@ -464,5 +595,6 @@ export function useControlCenterState(initialNodes, initialEdges) {
|
|
|
464
595
|
handleDeleteRepository,
|
|
465
596
|
isDeleting,
|
|
466
597
|
createFeatureNode,
|
|
598
|
+
selectFeatureById,
|
|
467
599
|
};
|
|
468
600
|
}
|
|
@@ -63,5 +63,5 @@ export function FeaturesCanvas({ nodes, edges, onNodesChange, onAddFeature, onNo
|
|
|
63
63
|
}
|
|
64
64
|
return (_jsx("div", { "data-testid": "features-canvas-empty", children: _jsx(EmptyState, { title: "No features yet", description: "Get started by creating your first feature.", action: _jsxs(Button, { onClick: onAddFeature, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Feature"] }) }) }));
|
|
65
65
|
}
|
|
66
|
-
return (_jsx("div", { "data-testid": "features-canvas", className: "h-full w-full", children: _jsx(ReactFlowProvider, { children: _jsxs(ReactFlow, { nodes: enrichedNodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, isValidConnection: isValidConnection, onConnect: onConnect, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, defaultViewport: { x: 30, y: 30, zoom: 0.85 }, nodesDraggable: false, nodesConnectable: false, elementsSelectable: false, children: [_jsx(Background, {}), _jsx(Controls, {}), onRepositorySelect ? (_jsx(Panel, { position: "top-right", className: "mr-
|
|
66
|
+
return (_jsx("div", { "data-testid": "features-canvas", className: "h-full w-full", children: _jsx(ReactFlowProvider, { children: _jsxs(ReactFlow, { nodes: enrichedNodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, isValidConnection: isValidConnection, onConnect: onConnect, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, defaultViewport: { x: 30, y: 30, zoom: 0.85 }, nodesDraggable: false, nodesConnectable: false, elementsSelectable: false, children: [_jsx(Background, {}), _jsx(Controls, {}), onRepositorySelect ? (_jsx(Panel, { position: "top-right", className: "mr-16", children: _jsx(AddRepositoryButton, { onSelect: onRepositorySelect }) })) : null, toolbar] }) }) }));
|
|
67
67
|
}
|
package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts
CHANGED
|
@@ -14,4 +14,5 @@ export declare const SingleRepositoryMultipleFeatures: Story;
|
|
|
14
14
|
export declare const MultipleRepositories: Story;
|
|
15
15
|
export declare const MultipleRepositoriesMixedConnections: Story;
|
|
16
16
|
export declare const InteractiveWithRepository: Story;
|
|
17
|
+
export declare const AddRepositoryButtonClearanceCheck: Story;
|
|
17
18
|
//# sourceMappingURL=features-canvas.stories.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features-canvas.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAOnD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAcrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAwKnC,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAKxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAK3B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAK5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAWzB,CAAC;AA4DF,eAAO,MAAM,mCAAmC,EAAE,KAOjD,CAAC;AAkCF,eAAO,MAAM,gCAAgC,EAAE,KAS9C,CAAC;AAgEF,eAAO,MAAM,gCAAgC,EAAE,KAW9C,CAAC;AAuDF,eAAO,MAAM,oBAAoB,EAAE,KAUlC,CAAC;AA2GF,eAAO,MAAM,oCAAoC,EAAE,KAOlD,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,KASvC,CAAC"}
|
|
1
|
+
{"version":3,"file":"features-canvas.stories.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAOnD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAcrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAwKnC,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAKxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAK3B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAK5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAWzB,CAAC;AA4DF,eAAO,MAAM,mCAAmC,EAAE,KAOjD,CAAC;AAkCF,eAAO,MAAM,gCAAgC,EAAE,KAS9C,CAAC;AAgEF,eAAO,MAAM,gCAAgC,EAAE,KAW9C,CAAC;AAuDF,eAAO,MAAM,oBAAoB,EAAE,KAUlC,CAAC;AA2GF,eAAO,MAAM,oCAAoC,EAAE,KAOlD,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,KASvC,CAAC;AAKF,eAAO,MAAM,iCAAiC,EAAE,KAoC/C,CAAC"}
|
package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { FeaturesCanvas } from './features-canvas.js';
|
|
3
3
|
import { layoutWithDagre } from '../../../lib/layout-with-dagre.js';
|
|
4
4
|
const meta = {
|
|
@@ -562,3 +562,33 @@ export const InteractiveWithRepository = {
|
|
|
562
562
|
onRepositoryAdd: () => undefined,
|
|
563
563
|
},
|
|
564
564
|
};
|
|
565
|
+
// Story to visually verify the Add Repository button does not overlap the ThemeToggle.
|
|
566
|
+
// The simulated ThemeToggle (top-3 right-3, 40×40 px) is rendered as an overlay so
|
|
567
|
+
// reviewers can confirm the mr-16 (64 px) gap keeps the Panel clear of the toggle zone.
|
|
568
|
+
export const AddRepositoryButtonClearanceCheck = {
|
|
569
|
+
decorators: [
|
|
570
|
+
(Story) => (_jsxs("div", { style: { height: '100vh', position: 'relative' }, children: [_jsx(Story, {}), _jsx("div", { style: {
|
|
571
|
+
position: 'absolute',
|
|
572
|
+
top: 12,
|
|
573
|
+
right: 12,
|
|
574
|
+
width: 40,
|
|
575
|
+
height: 40,
|
|
576
|
+
background: 'rgba(255, 0, 0, 0.35)',
|
|
577
|
+
border: '2px dashed red',
|
|
578
|
+
borderRadius: 6,
|
|
579
|
+
display: 'flex',
|
|
580
|
+
alignItems: 'center',
|
|
581
|
+
justifyContent: 'center',
|
|
582
|
+
fontSize: 10,
|
|
583
|
+
pointerEvents: 'none',
|
|
584
|
+
zIndex: 50,
|
|
585
|
+
}, children: "Theme" })] })),
|
|
586
|
+
],
|
|
587
|
+
args: {
|
|
588
|
+
nodes: repoFeatureNodes,
|
|
589
|
+
edges: repoFeatureEdges,
|
|
590
|
+
onNodeAction: () => undefined,
|
|
591
|
+
onNodeSettings: () => undefined,
|
|
592
|
+
onRepositorySelect: () => undefined,
|
|
593
|
+
},
|
|
594
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/layouts/app-shell/app-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/layouts/app-shell/app-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAQpD,UAAU,aAAa;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB;AA2BD,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,2CAMnD"}
|
|
@@ -4,6 +4,7 @@ import { useCallback } from 'react';
|
|
|
4
4
|
import { SidebarProvider, SidebarInset } from '../../ui/sidebar.js';
|
|
5
5
|
import { AppSidebar } from '../../layouts/app-sidebar/index.js';
|
|
6
6
|
import { ThemeToggle } from '../../common/theme-toggle/index.js';
|
|
7
|
+
import { SoundToggle } from '../../common/sound-toggle/index.js';
|
|
7
8
|
import { AgentEventsProvider } from '../../../hooks/agent-events-provider.js';
|
|
8
9
|
import { useNotifications } from '../../../hooks/use-notifications.js';
|
|
9
10
|
/** Inner shell that consumes the agent-events context for notifications. */
|
|
@@ -13,7 +14,7 @@ function AppShellInner({ children }) {
|
|
|
13
14
|
const handleNewFeature = useCallback(() => {
|
|
14
15
|
window.dispatchEvent(new CustomEvent('shep:open-create-drawer'));
|
|
15
16
|
}, []);
|
|
16
|
-
return (_jsxs(SidebarProvider, { children: [_jsx(AppSidebar, { features: [], onNewFeature: handleNewFeature }), _jsx(SidebarInset, { children: _jsxs("div", { className: "relative h-full", children: [
|
|
17
|
+
return (_jsxs(SidebarProvider, { children: [_jsx(AppSidebar, { features: [], onNewFeature: handleNewFeature }), _jsx(SidebarInset, { children: _jsxs("div", { className: "relative h-full", children: [_jsxs("div", { className: "absolute top-3 right-3 z-50 flex gap-1", children: [_jsx(SoundToggle, {}), _jsx(ThemeToggle, {})] }), _jsx("main", { className: "h-full", children: children })] }) })] }));
|
|
17
18
|
}
|
|
18
19
|
export function AppShell({ children }) {
|
|
19
20
|
return (_jsx(AgentEventsProvider, { children: _jsx(AppShellInner, { children: children }) }));
|
|
@@ -17,6 +17,7 @@ import path from 'node:path';
|
|
|
17
17
|
import { initializeContainer, container } from '../../../packages/core/src/infrastructure/di/container.js';
|
|
18
18
|
import { InitializeSettingsUseCase } from '../../../packages/core/src/application/use-cases/settings/initialize-settings.use-case.js';
|
|
19
19
|
import { initializeSettings } from '../../../packages/core/src/infrastructure/services/settings.service.js';
|
|
20
|
+
import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
|
|
20
21
|
const DEFAULT_PORT = 3000;
|
|
21
22
|
async function isPortAvailable(port) {
|
|
22
23
|
return new Promise((resolve) => {
|
|
@@ -53,6 +54,13 @@ async function main() {
|
|
|
53
54
|
const initSettingsUseCase = container.resolve(InitializeSettingsUseCase);
|
|
54
55
|
const settings = await initSettingsUseCase.execute();
|
|
55
56
|
initializeSettings(settings);
|
|
57
|
+
// Start notification watcher for real-time SSE events (same as shep ui)
|
|
58
|
+
const runRepo = container.resolve('IAgentRunRepository');
|
|
59
|
+
const phaseTimingRepo = container.resolve('IPhaseTimingRepository');
|
|
60
|
+
const featureRepo = container.resolve('IFeatureRepository');
|
|
61
|
+
const notificationService = container.resolve('INotificationService');
|
|
62
|
+
initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
|
|
63
|
+
getNotificationWatcher().start();
|
|
56
64
|
}
|
|
57
65
|
catch (error) {
|
|
58
66
|
console.warn('[dev-server] DI initialization failed — features will be empty:', error);
|
|
@@ -92,6 +100,12 @@ async function main() {
|
|
|
92
100
|
console.log('\n[dev-server] Shutting down...');
|
|
93
101
|
const forceExit = setTimeout(() => process.exit(0), 2000);
|
|
94
102
|
try {
|
|
103
|
+
try {
|
|
104
|
+
getNotificationWatcher().stop();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* not initialized */
|
|
108
|
+
}
|
|
95
109
|
server.closeAllConnections();
|
|
96
110
|
await Promise.all([
|
|
97
111
|
new Promise((resolve) => server.close(() => resolve())),
|
|
@@ -8,5 +8,12 @@ export interface UseAgentEventsResult {
|
|
|
8
8
|
lastEvent: NotificationEvent | null;
|
|
9
9
|
connectionStatus: ConnectionStatus;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Hook that receives real-time agent notification events via a Service Worker.
|
|
13
|
+
*
|
|
14
|
+
* The SW maintains a single SSE connection to /api/agent-events and
|
|
15
|
+
* broadcasts events to all open tabs. Falls back to direct EventSource
|
|
16
|
+
* when Service Workers are unavailable.
|
|
17
|
+
*/
|
|
11
18
|
export declare function useAgentEvents(options?: UseAgentEventsOptions): UseAgentEventsResult;
|
|
12
19
|
//# sourceMappingURL=use-agent-events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-events.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/web/hooks/use-agent-events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,YAAY,GAAG,cAAc,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,SAAS,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"use-agent-events.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/web/hooks/use-agent-events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,YAAY,GAAG,cAAc,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,SAAS,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,oBAAoB,CA2GpF"}
|
|
@@ -1,53 +1,143 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const SW_PATH = '/agent-events-sw.js';
|
|
4
|
+
/**
|
|
5
|
+
* Hook that receives real-time agent notification events via a Service Worker.
|
|
6
|
+
*
|
|
7
|
+
* The SW maintains a single SSE connection to /api/agent-events and
|
|
8
|
+
* broadcasts events to all open tabs. Falls back to direct EventSource
|
|
9
|
+
* when Service Workers are unavailable.
|
|
10
|
+
*/
|
|
7
11
|
export function useAgentEvents(options) {
|
|
8
12
|
const [events, setEvents] = useState([]);
|
|
9
13
|
const [lastEvent, setLastEvent] = useState(null);
|
|
10
14
|
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
|
11
|
-
const backoffRef = useRef(BASE_BACKOFF_MS);
|
|
12
|
-
const reconnectTimerRef = useRef(null);
|
|
13
|
-
const stableTimerRef = useRef(null);
|
|
14
|
-
const esRef = useRef(null);
|
|
15
15
|
const runId = options?.runId;
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const swRef = useRef(null);
|
|
17
|
+
const onMessage = useCallback((event) => {
|
|
18
|
+
const msg = event.data;
|
|
19
|
+
if (!msg || typeof msg !== 'object')
|
|
18
20
|
return;
|
|
21
|
+
if (msg.type === 'notification') {
|
|
22
|
+
const parsed = msg.data;
|
|
23
|
+
setEvents((prev) => [...prev, parsed]);
|
|
24
|
+
setLastEvent(parsed);
|
|
19
25
|
}
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
else if (msg.type === 'status') {
|
|
27
|
+
setConnectionStatus(msg.status);
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (typeof window === 'undefined')
|
|
22
32
|
return;
|
|
33
|
+
// ?sse=direct bypasses the Service Worker for debugging
|
|
34
|
+
const bypassSW = typeof location !== 'undefined' && new URLSearchParams(location.search).has('sse');
|
|
35
|
+
// Fallback: direct EventSource when SW not supported or bypassed
|
|
36
|
+
if (!navigator.serviceWorker || bypassSW) {
|
|
37
|
+
return connectDirectEventSource(runId, setEvents, setLastEvent, setConnectionStatus);
|
|
23
38
|
}
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
const fallbackCleanupRef = { current: undefined };
|
|
41
|
+
function subscribeToWorker(worker) {
|
|
42
|
+
if (cancelled)
|
|
43
|
+
return;
|
|
44
|
+
swRef.current = worker;
|
|
45
|
+
worker.postMessage({ type: 'subscribe', runId });
|
|
46
|
+
setConnectionStatus('connecting');
|
|
47
|
+
}
|
|
48
|
+
// Listen for messages from whatever SW controls this page
|
|
49
|
+
navigator.serviceWorker.addEventListener('message', onMessage);
|
|
50
|
+
// Re-subscribe when SW controller changes (e.g., after SW update via skipWaiting)
|
|
51
|
+
function handleControllerChange() {
|
|
52
|
+
if (cancelled)
|
|
53
|
+
return;
|
|
54
|
+
const newController = navigator.serviceWorker.controller;
|
|
55
|
+
if (newController) {
|
|
56
|
+
swRef.current = newController;
|
|
57
|
+
newController.postMessage({ type: 'subscribe', runId });
|
|
58
|
+
setConnectionStatus('connecting');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
navigator.serviceWorker.addEventListener('controllerchange', handleControllerChange);
|
|
62
|
+
navigator.serviceWorker
|
|
63
|
+
.register(SW_PATH, { scope: '/' })
|
|
64
|
+
.then((registration) => {
|
|
65
|
+
if (cancelled)
|
|
66
|
+
return;
|
|
67
|
+
// Use the active worker, or wait for it to activate
|
|
68
|
+
const sw = registration.active ?? registration.installing ?? registration.waiting;
|
|
69
|
+
if (!sw) {
|
|
70
|
+
// No worker at all — fall back to direct EventSource
|
|
71
|
+
fallbackCleanupRef.current = connectDirectEventSource(runId, setEvents, setLastEvent, setConnectionStatus);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (sw.state === 'activated') {
|
|
75
|
+
subscribeToWorker(sw);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
sw.addEventListener('statechange', () => {
|
|
79
|
+
if (sw.state === 'activated')
|
|
80
|
+
subscribeToWorker(sw);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(() => {
|
|
85
|
+
// SW registration failed — fall back to direct EventSource
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
fallbackCleanupRef.current = connectDirectEventSource(runId, setEvents, setLastEvent, setConnectionStatus);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true;
|
|
92
|
+
navigator.serviceWorker.removeEventListener('message', onMessage);
|
|
93
|
+
navigator.serviceWorker.removeEventListener('controllerchange', handleControllerChange);
|
|
94
|
+
swRef.current?.postMessage({ type: 'unsubscribe' });
|
|
95
|
+
swRef.current = null;
|
|
96
|
+
fallbackCleanupRef.current?.();
|
|
97
|
+
};
|
|
98
|
+
}, [runId, onMessage]);
|
|
99
|
+
return { events, lastEvent, connectionStatus };
|
|
100
|
+
}
|
|
101
|
+
// --- EventSource fallback (identical to the previous implementation) ---
|
|
102
|
+
const BASE_BACKOFF_MS = 1000;
|
|
103
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
104
|
+
const STABLE_CONNECTION_MS = 5_000;
|
|
105
|
+
function connectDirectEventSource(runId, setEvents, setLastEvent, setConnectionStatus) {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
107
|
+
if (typeof EventSource === 'undefined')
|
|
108
|
+
return () => { };
|
|
109
|
+
let es = null;
|
|
110
|
+
let stopped = false;
|
|
111
|
+
let backoff = BASE_BACKOFF_MS;
|
|
112
|
+
let reconnectTimer = null;
|
|
113
|
+
let stableTimer = null;
|
|
114
|
+
function connect() {
|
|
115
|
+
if (stopped)
|
|
116
|
+
return;
|
|
24
117
|
const url = runId
|
|
25
118
|
? `/api/agent-events?runId=${encodeURIComponent(runId)}`
|
|
26
119
|
: '/api/agent-events';
|
|
27
|
-
|
|
28
|
-
esRef.current = es;
|
|
120
|
+
es = new EventSource(url);
|
|
29
121
|
setConnectionStatus('connecting');
|
|
30
122
|
es.onopen = () => {
|
|
31
123
|
setConnectionStatus('connected');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
backoffRef.current = BASE_BACKOFF_MS;
|
|
124
|
+
stableTimer = setTimeout(() => {
|
|
125
|
+
stableTimer = null;
|
|
126
|
+
backoff = BASE_BACKOFF_MS;
|
|
36
127
|
}, STABLE_CONNECTION_MS);
|
|
37
128
|
};
|
|
38
129
|
es.onerror = () => {
|
|
39
|
-
es
|
|
40
|
-
|
|
130
|
+
es?.close();
|
|
131
|
+
es = null;
|
|
41
132
|
setConnectionStatus('disconnected');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
stableTimerRef.current = null;
|
|
133
|
+
if (stableTimer !== null) {
|
|
134
|
+
clearTimeout(stableTimer);
|
|
135
|
+
stableTimer = null;
|
|
46
136
|
}
|
|
47
|
-
const delay =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
137
|
+
const delay = backoff;
|
|
138
|
+
backoff = Math.min(delay * 2, MAX_BACKOFF_MS);
|
|
139
|
+
reconnectTimer = setTimeout(() => {
|
|
140
|
+
reconnectTimer = null;
|
|
51
141
|
connect();
|
|
52
142
|
}, delay);
|
|
53
143
|
};
|
|
@@ -56,23 +146,21 @@ export function useAgentEvents(options) {
|
|
|
56
146
|
setEvents((prev) => [...prev, parsed]);
|
|
57
147
|
setLastEvent(parsed);
|
|
58
148
|
}));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}, [connect]);
|
|
77
|
-
return { events, lastEvent, connectionStatus };
|
|
149
|
+
}
|
|
150
|
+
connect();
|
|
151
|
+
return () => {
|
|
152
|
+
stopped = true;
|
|
153
|
+
if (reconnectTimer !== null) {
|
|
154
|
+
clearTimeout(reconnectTimer);
|
|
155
|
+
reconnectTimer = null;
|
|
156
|
+
}
|
|
157
|
+
if (stableTimer !== null) {
|
|
158
|
+
clearTimeout(stableTimer);
|
|
159
|
+
stableTimer = null;
|
|
160
|
+
}
|
|
161
|
+
if (es) {
|
|
162
|
+
es.close();
|
|
163
|
+
es = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
78
166
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-notifications.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/web/hooks/use-notifications.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-notifications.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/web/hooks/use-notifications.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,sBAAsB;IACrC,wBAAwB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,sBAAsB,EAAE,sBAAsB,CAAC;CAChD;AA4CD,wBAAgB,gBAAgB,IAAI,sBAAsB,CAgEzD"}
|