@shepai/cli 1.72.9-pr219.08421ee → 1.72.9-pr220.68aacbc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.d.ts +20 -1
  2. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.d.ts.map +1 -1
  3. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.js +68 -41
  4. package/dist/packages/core/src/application/use-cases/features/create/types.d.ts +10 -0
  5. package/dist/packages/core/src/application/use-cases/features/create/types.d.ts.map +1 -1
  6. package/dist/packages/core/src/infrastructure/services/notifications/notification.service.d.ts.map +1 -1
  7. package/dist/packages/core/src/infrastructure/services/notifications/notification.service.js +2 -4
  8. package/dist/src/presentation/web/app/actions/create-feature.d.ts.map +1 -1
  9. package/dist/src/presentation/web/app/actions/create-feature.js +20 -2
  10. package/dist/src/presentation/web/app/actions/get-feature-metadata.d.ts +10 -0
  11. package/dist/src/presentation/web/app/actions/get-feature-metadata.d.ts.map +1 -0
  12. package/dist/src/presentation/web/app/actions/get-feature-metadata.js +14 -0
  13. package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -1
  14. package/dist/src/presentation/web/app/api/agent-events/route.js +16 -17
  15. package/dist/src/presentation/web/app/build-feature-node-data.d.ts.map +1 -1
  16. package/dist/src/presentation/web/app/build-feature-node-data.js +2 -26
  17. package/dist/src/presentation/web/app/build-graph-nodes.d.ts.map +1 -1
  18. package/dist/src/presentation/web/app/build-graph-nodes.js +2 -26
  19. package/dist/src/presentation/web/components/common/add-repository-button/add-repository-button.d.ts.map +1 -0
  20. package/dist/src/presentation/web/components/common/add-repository-button/index.d.ts +2 -0
  21. package/dist/src/presentation/web/components/common/add-repository-button/index.d.ts.map +1 -0
  22. package/dist/src/presentation/web/components/common/{add-repository-node → add-repository-button}/index.js +0 -1
  23. package/dist/src/presentation/web/components/common/add-repository-button/pick-folder.d.ts.map +1 -0
  24. package/dist/src/presentation/web/components/common/control-center-drawer/create-drawer-client.d.ts.map +1 -1
  25. package/dist/src/presentation/web/components/common/control-center-drawer/create-drawer-client.js +13 -18
  26. package/dist/src/presentation/web/components/common/control-center-drawer/drawer-view.d.ts +2 -0
  27. package/dist/src/presentation/web/components/common/control-center-drawer/drawer-view.d.ts.map +1 -1
  28. package/dist/src/presentation/web/components/common/control-center-drawer/drawer-view.js +11 -11
  29. package/dist/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.d.ts.map +1 -1
  30. package/dist/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.js +69 -141
  31. package/dist/src/presentation/web/components/common/control-center-drawer/index.d.ts +0 -1
  32. package/dist/src/presentation/web/components/common/control-center-drawer/index.d.ts.map +1 -1
  33. package/dist/src/presentation/web/components/common/control-center-drawer/index.js +0 -1
  34. package/dist/src/presentation/web/components/common/control-center-drawer/use-artifact-fetch.d.ts +13 -0
  35. package/dist/src/presentation/web/components/common/control-center-drawer/use-artifact-fetch.d.ts.map +1 -0
  36. package/dist/src/presentation/web/components/common/control-center-drawer/use-artifact-fetch.js +46 -0
  37. package/dist/src/presentation/web/components/common/feature-drawer/use-feature-actions.d.ts.map +1 -1
  38. package/dist/src/presentation/web/components/common/feature-drawer/use-feature-actions.js +7 -10
  39. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts +25 -0
  40. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  41. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +45 -2
  42. package/dist/src/presentation/web/components/common/index.d.ts +2 -3
  43. package/dist/src/presentation/web/components/common/index.d.ts.map +1 -1
  44. package/dist/src/presentation/web/components/common/index.js +2 -3
  45. package/dist/src/presentation/web/components/common/repository-node/use-repository-actions.d.ts.map +1 -1
  46. package/dist/src/presentation/web/components/common/repository-node/use-repository-actions.js +14 -39
  47. package/dist/src/presentation/web/components/features/control-center/control-center-empty-state.js +1 -1
  48. package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
  49. package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +31 -14
  50. package/dist/src/presentation/web/components/features/control-center/control-center.stories.d.ts +0 -11
  51. package/dist/src/presentation/web/components/features/control-center/control-center.stories.d.ts.map +1 -1
  52. package/dist/src/presentation/web/components/features/control-center/control-center.stories.js +2 -37
  53. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +8 -0
  54. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
  55. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +158 -396
  56. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.d.ts +2 -10
  57. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.d.ts.map +1 -1
  58. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.js +11 -54
  59. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.d.ts.map +1 -1
  60. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.stories.js +0 -46
  61. package/dist/src/presentation/web/components/layouts/app-shell/app-shell.js +1 -1
  62. package/dist/src/presentation/web/components/ui/alert-dialog.d.ts.map +1 -1
  63. package/dist/src/presentation/web/components/ui/alert-dialog.js +2 -2
  64. package/dist/src/presentation/web/components/ui/dialog.d.ts.map +1 -1
  65. package/dist/src/presentation/web/components/ui/dialog.js +2 -2
  66. package/dist/src/presentation/web/hooks/agent-events-provider.d.ts.map +1 -1
  67. package/dist/src/presentation/web/hooks/agent-events-provider.js +3 -2
  68. package/dist/src/presentation/web/hooks/use-agent-events.d.ts.map +1 -1
  69. package/dist/src/presentation/web/hooks/use-agent-events.js +16 -5
  70. package/dist/src/presentation/web/hooks/use-deploy-action.d.ts.map +1 -1
  71. package/dist/src/presentation/web/hooks/use-deploy-action.js +8 -3
  72. package/dist/src/presentation/web/hooks/use-graph-state.d.ts +42 -0
  73. package/dist/src/presentation/web/hooks/use-graph-state.d.ts.map +1 -0
  74. package/dist/src/presentation/web/hooks/use-graph-state.js +257 -0
  75. package/dist/src/presentation/web/hooks/use-notifications.d.ts +1 -5
  76. package/dist/src/presentation/web/hooks/use-notifications.d.ts.map +1 -1
  77. package/dist/src/presentation/web/hooks/use-notifications.js +1 -26
  78. package/dist/src/presentation/web/hooks/use-notifications.stories.d.ts.map +1 -1
  79. package/dist/src/presentation/web/hooks/use-notifications.stories.js +28 -38
  80. package/dist/src/presentation/web/lib/derive-graph.d.ts +52 -0
  81. package/dist/src/presentation/web/lib/derive-graph.d.ts.map +1 -0
  82. package/dist/src/presentation/web/lib/derive-graph.js +126 -0
  83. package/dist/src/presentation/web/lib/layout-with-dagre.d.ts.map +1 -1
  84. package/dist/src/presentation/web/lib/layout-with-dagre.js +0 -1
  85. package/dist/tsconfig.build.tsbuildinfo +1 -1
  86. package/package.json +1 -1
  87. package/web/.next/BUILD_ID +1 -1
  88. package/web/.next/build-manifest.json +2 -2
  89. package/web/.next/cache/.previewinfo +1 -1
  90. package/web/.next/cache/.rscinfo +1 -1
  91. package/web/.next/cache/.tsbuildinfo +1 -1
  92. package/web/.next/cache/config.json +3 -3
  93. package/web/.next/fallback-build-manifest.json +2 -2
  94. package/web/.next/prerender-manifest.json +3 -3
  95. package/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +34 -34
  96. package/web/.next/server/app/(dashboard)/@drawer/create/page.js +3 -3
  97. package/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
  98. package/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
  99. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +40 -40
  100. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js +3 -3
  101. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
  102. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
  103. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +30 -30
  104. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js +3 -3
  105. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
  106. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  107. package/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +34 -34
  108. package/web/.next/server/app/(dashboard)/create/page.js +3 -3
  109. package/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
  110. package/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
  111. package/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +40 -40
  112. package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js +3 -3
  113. package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
  114. package/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
  115. package/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +30 -30
  116. package/web/.next/server/app/(dashboard)/page.js +3 -3
  117. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  118. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  119. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +30 -30
  120. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js +3 -3
  121. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
  122. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  123. package/web/.next/server/app/_global-error.html +2 -2
  124. package/web/.next/server/app/_global-error.rsc +1 -1
  125. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  126. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  127. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  128. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  129. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  130. package/web/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  131. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  132. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  133. package/web/.next/server/app/skills/page/server-reference-manifest.json +6 -6
  134. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  135. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  136. package/web/.next/server/app/tools/page/server-reference-manifest.json +6 -6
  137. package/web/.next/server/app/tools/page.js.nft.json +1 -1
  138. package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  139. package/web/.next/server/app/version/page/server-reference-manifest.json +1 -1
  140. package/web/.next/server/app/version/page.js.nft.json +1 -1
  141. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  142. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
  143. package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js.map +1 -1
  144. package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js +1 -1
  145. package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js.map +1 -1
  146. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_@drawer_repository_[repositoryId]_page_actions_816618e2.js +3 -0
  147. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_@drawer_repository_[repositoryId]_page_actions_816618e2.js.map +1 -0
  148. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_repository_[repositoryId]_page_actions_ba2e050f.js +3 -0
  149. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_repository_[repositoryId]_page_actions_ba2e050f.js.map +1 -0
  150. package/web/.next/server/chunks/ssr/[root-of-the-server]__17a611f0._.js +2 -2
  151. package/web/.next/server/chunks/ssr/[root-of-the-server]__17a611f0._.js.map +1 -1
  152. package/web/.next/server/chunks/ssr/[root-of-the-server]__2018e25f._.js +1 -1
  153. package/web/.next/server/chunks/ssr/[root-of-the-server]__2018e25f._.js.map +1 -1
  154. package/web/.next/server/chunks/ssr/[root-of-the-server]__45dbf93d._.js +2 -2
  155. package/web/.next/server/chunks/ssr/[root-of-the-server]__45dbf93d._.js.map +1 -1
  156. package/web/.next/server/chunks/ssr/{[root-of-the-server]__cbae9863._.js → [root-of-the-server]__6258349c._.js} +2 -2
  157. package/web/.next/server/chunks/ssr/[root-of-the-server]__6258349c._.js.map +1 -0
  158. package/web/.next/server/chunks/ssr/[root-of-the-server]__6b17a22d._.js +1 -1
  159. package/web/.next/server/chunks/ssr/[root-of-the-server]__6b17a22d._.js.map +1 -1
  160. package/web/.next/server/chunks/ssr/[root-of-the-server]__6cbc1149._.js +2 -2
  161. package/web/.next/server/chunks/ssr/[root-of-the-server]__6cbc1149._.js.map +1 -1
  162. package/web/.next/server/chunks/ssr/[root-of-the-server]__804c006d._.js +1 -1
  163. package/web/.next/server/chunks/ssr/[root-of-the-server]__804c006d._.js.map +1 -1
  164. package/web/.next/{standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__e5e09f72._.js → server/chunks/ssr/[root-of-the-server]__94a6d275._.js} +2 -2
  165. package/web/.next/server/chunks/ssr/[root-of-the-server]__94a6d275._.js.map +1 -0
  166. package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js +1 -1
  167. package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js.map +1 -1
  168. package/web/.next/server/chunks/ssr/_07c2a32a._.js +1 -1
  169. package/web/.next/server/chunks/ssr/_07c2a32a._.js.map +1 -1
  170. package/web/.next/server/chunks/ssr/_0e9d6bf7._.js +1 -1
  171. package/web/.next/server/chunks/ssr/_0e9d6bf7._.js.map +1 -1
  172. package/web/.next/server/chunks/ssr/_250a63ae._.js +3 -0
  173. package/web/.next/server/chunks/ssr/_250a63ae._.js.map +1 -0
  174. package/web/.next/server/chunks/ssr/_61351ed3._.js +1 -1
  175. package/web/.next/server/chunks/ssr/_61351ed3._.js.map +1 -1
  176. package/web/.next/server/chunks/ssr/_8400c8bb._.js +1 -1
  177. package/web/.next/server/chunks/ssr/_8400c8bb._.js.map +1 -1
  178. package/web/.next/server/chunks/ssr/_9f940437._.js +1 -1
  179. package/web/.next/server/chunks/ssr/_9f940437._.js.map +1 -1
  180. package/web/.next/server/chunks/ssr/_ad111e34._.js +6 -0
  181. package/web/.next/server/chunks/ssr/_ad111e34._.js.map +1 -0
  182. package/web/.next/{standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_5ea689db._.js → server/chunks/ssr/src_presentation_web_2b91e4ec._.js} +2 -2
  183. package/web/.next/server/chunks/ssr/{src_presentation_web_7188a290._.js.map → src_presentation_web_2b91e4ec._.js.map} +1 -1
  184. package/web/.next/server/chunks/ssr/{src_presentation_web_5ea689db._.js → src_presentation_web_5bcc1537._.js} +2 -2
  185. package/web/.next/server/chunks/ssr/{src_presentation_web_5ea689db._.js.map → src_presentation_web_5bcc1537._.js.map} +1 -1
  186. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_5955c012.js +3 -0
  187. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_5955c012.js.map +1 -0
  188. package/web/.next/server/chunks/ssr/src_presentation_web_ca99d62d._.js +3 -0
  189. package/web/.next/server/chunks/ssr/src_presentation_web_ca99d62d._.js.map +1 -0
  190. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  191. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  192. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js +1 -1
  193. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js.map +1 -1
  194. package/web/.next/server/chunks/ssr/src_presentation_web_d5b0cb86._.js +8 -0
  195. package/web/.next/server/chunks/ssr/src_presentation_web_d5b0cb86._.js.map +1 -0
  196. package/web/.next/server/chunks/ssr/src_presentation_web_e626b63c._.js +8 -0
  197. package/web/.next/server/chunks/ssr/src_presentation_web_e626b63c._.js.map +1 -0
  198. package/web/.next/server/pages/500.html +2 -2
  199. package/web/.next/server/server-reference-manifest.js +1 -1
  200. package/web/.next/server/server-reference-manifest.json +142 -142
  201. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  202. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  203. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  204. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +34 -34
  205. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/create/page.js +3 -3
  206. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
  207. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
  208. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +40 -40
  209. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js +3 -3
  210. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
  211. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
  212. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +30 -30
  213. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js +3 -3
  214. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
  215. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  216. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +34 -34
  217. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/create/page.js +3 -3
  218. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
  219. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
  220. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +40 -40
  221. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/feature/[featureId]/page.js +3 -3
  222. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
  223. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
  224. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +30 -30
  225. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/page.js +3 -3
  226. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  227. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  228. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +30 -30
  229. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js +3 -3
  230. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
  231. package/web/.next/standalone/src/presentation/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  232. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  233. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  234. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  235. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  236. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  237. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  238. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  239. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  240. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  241. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  242. package/web/.next/standalone/src/presentation/web/.next/server/app/skills/page/server-reference-manifest.json +6 -6
  243. package/web/.next/standalone/src/presentation/web/.next/server/app/skills/page.js.nft.json +1 -1
  244. package/web/.next/standalone/src/presentation/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  245. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page/server-reference-manifest.json +6 -6
  246. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page.js.nft.json +1 -1
  247. package/web/.next/standalone/src/presentation/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  248. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page/server-reference-manifest.json +1 -1
  249. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page.js.nft.json +1 -1
  250. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  251. package/web/.next/standalone/src/presentation/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_4d623b8e.js +2 -2
  252. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js +1 -1
  253. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_@drawer_repository_[repositoryId]_page_actions_816618e2.js +3 -0
  254. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_repository_[repositoryId]_page_actions_ba2e050f.js +3 -0
  255. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__17a611f0._.js +2 -2
  256. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2018e25f._.js +1 -1
  257. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__45dbf93d._.js +2 -2
  258. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{[root-of-the-server]__cbae9863._.js → [root-of-the-server]__6258349c._.js} +2 -2
  259. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6b17a22d._.js +1 -1
  260. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6cbc1149._.js +2 -2
  261. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__804c006d._.js +1 -1
  262. package/web/.next/{server/chunks/ssr/[root-of-the-server]__e5e09f72._.js → standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__94a6d275._.js} +2 -2
  263. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js +1 -1
  264. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_07c2a32a._.js +1 -1
  265. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_0e9d6bf7._.js +1 -1
  266. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_250a63ae._.js +3 -0
  267. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_61351ed3._.js +1 -1
  268. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_8400c8bb._.js +1 -1
  269. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_9f940437._.js +1 -1
  270. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_ad111e34._.js +6 -0
  271. package/web/.next/{server/chunks/ssr/src_presentation_web_7188a290._.js → standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_2b91e4ec._.js} +2 -2
  272. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/{src_presentation_web_7188a290._.js → src_presentation_web_5bcc1537._.js} +2 -2
  273. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_5955c012.js +3 -0
  274. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ca99d62d._.js +3 -0
  275. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  276. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js +1 -1
  277. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_d5b0cb86._.js +8 -0
  278. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_e626b63c._.js +8 -0
  279. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  280. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  281. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +142 -142
  282. package/web/.next/standalone/src/presentation/web/CLAUDE.md +77 -33
  283. package/web/.next/standalone/src/presentation/web/app/actions/create-feature.ts +27 -2
  284. package/web/.next/standalone/src/presentation/web/app/actions/get-feature-metadata.ts +18 -0
  285. package/web/.next/standalone/src/presentation/web/app/api/agent-events/route.ts +18 -19
  286. package/web/.next/standalone/src/presentation/web/app/build-feature-node-data.ts +3 -30
  287. package/web/.next/standalone/src/presentation/web/app/build-graph-nodes.ts +3 -30
  288. package/web/.next/standalone/src/presentation/web/components/common/add-repository-button/index.ts +1 -0
  289. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/create-drawer-client.tsx +15 -21
  290. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/drawer-view.ts +12 -13
  291. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/feature-drawer-client.tsx +99 -137
  292. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/index.ts +0 -1
  293. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/use-artifact-fetch.ts +56 -0
  294. package/web/.next/standalone/src/presentation/web/components/common/feature-drawer/use-feature-actions.ts +6 -7
  295. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +63 -2
  296. package/web/.next/standalone/src/presentation/web/components/common/index.ts +2 -12
  297. package/web/.next/standalone/src/presentation/web/components/common/repository-node/use-repository-actions.ts +29 -54
  298. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-empty-state.tsx +1 -1
  299. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +43 -36
  300. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center.stories.tsx +2 -41
  301. package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +202 -495
  302. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.stories.tsx +0 -48
  303. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +13 -76
  304. package/web/.next/standalone/src/presentation/web/components/layouts/app-shell/app-shell.tsx +1 -1
  305. package/web/.next/standalone/src/presentation/web/components/ui/alert-dialog.tsx +2 -5
  306. package/web/.next/standalone/src/presentation/web/components/ui/dialog.tsx +2 -5
  307. package/web/.next/standalone/src/presentation/web/hooks/agent-events-provider.tsx +7 -2
  308. package/web/.next/standalone/src/presentation/web/hooks/use-agent-events.ts +17 -5
  309. package/web/.next/standalone/src/presentation/web/hooks/use-deploy-action.ts +11 -3
  310. package/web/.next/standalone/src/presentation/web/hooks/use-graph-state.ts +376 -0
  311. package/web/.next/standalone/src/presentation/web/hooks/use-notifications.stories.tsx +2 -30
  312. package/web/.next/standalone/src/presentation/web/hooks/use-notifications.ts +2 -36
  313. package/web/.next/standalone/src/presentation/web/lib/derive-graph.ts +178 -0
  314. package/web/.next/standalone/src/presentation/web/lib/layout-with-dagre.ts +0 -1
  315. package/web/.next/static/chunks/0ffccd7be73305c8.css +2 -0
  316. package/web/.next/static/chunks/3e25d4932e1dd406.js +1 -0
  317. package/web/.next/static/chunks/3f205fd6adbfd20a.js +7 -0
  318. package/web/.next/static/chunks/68ded4375df20ac5.js +1 -0
  319. package/web/.next/static/chunks/{1d281f045b623a45.js → 6c13863b031221ea.js} +1 -1
  320. package/web/.next/static/chunks/8ecedb1a498fb468.js +1 -0
  321. package/web/.next/static/chunks/95682021c08d3636.js +1 -0
  322. package/web/.next/static/chunks/ab808dc1cdf66a9c.js +4 -0
  323. package/web/.next/static/chunks/cb41849bcfaa304e.js +1 -0
  324. package/web/.next/static/chunks/{cd9099d3eae9dd04.js → ee1a546ece509b1d.js} +1 -1
  325. package/web/.next/static/chunks/f200e62f7185c8da.js +1 -0
  326. package/web/.next/static/chunks/fc0f263fd83b43f9.js +1 -0
  327. package/web/.next/trace +1 -1
  328. package/web/.next/trace-build +1 -1
  329. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.d.ts.map +0 -1
  330. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node-config.d.ts +0 -7
  331. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node-config.d.ts.map +0 -1
  332. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node-config.js +0 -1
  333. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.d.ts +0 -6
  334. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.d.ts.map +0 -1
  335. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.js +0 -23
  336. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.d.ts +0 -10
  337. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.d.ts.map +0 -1
  338. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.js +0 -33
  339. package/dist/src/presentation/web/components/common/add-repository-node/index.d.ts +0 -4
  340. package/dist/src/presentation/web/components/common/add-repository-node/index.d.ts.map +0 -1
  341. package/dist/src/presentation/web/components/common/add-repository-node/pick-folder.d.ts.map +0 -1
  342. package/dist/src/presentation/web/components/common/control-center-drawer/control-center-drawer.d.ts +0 -12
  343. package/dist/src/presentation/web/components/common/control-center-drawer/control-center-drawer.d.ts.map +0 -1
  344. package/dist/src/presentation/web/components/common/control-center-drawer/control-center-drawer.js +0 -337
  345. package/dist/src/presentation/web/components/common/control-center-drawer/control-center-drawer.stories.d.ts +0 -33
  346. package/dist/src/presentation/web/components/common/control-center-drawer/control-center-drawer.stories.d.ts.map +0 -1
  347. package/dist/src/presentation/web/components/common/control-center-drawer/control-center-drawer.stories.js +0 -214
  348. package/dist/src/presentation/web/components/common/notification-permission-banner/index.d.ts +0 -2
  349. package/dist/src/presentation/web/components/common/notification-permission-banner/index.d.ts.map +0 -1
  350. package/dist/src/presentation/web/components/common/notification-permission-banner/index.js +0 -1
  351. package/dist/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.d.ts +0 -2
  352. package/dist/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.d.ts.map +0 -1
  353. package/dist/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.js +0 -33
  354. package/dist/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.stories.d.ts +0 -6
  355. package/dist/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.stories.d.ts.map +0 -1
  356. package/dist/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.stories.js +0 -24
  357. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_@drawer_repository_[repositoryId]_page_actions_3a02d8a5.js +0 -3
  358. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_@drawer_repository_[repositoryId]_page_actions_3a02d8a5.js.map +0 -1
  359. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_repository_[repositoryId]_page_actions_a3044852.js +0 -3
  360. package/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_repository_[repositoryId]_page_actions_a3044852.js.map +0 -1
  361. package/web/.next/server/chunks/ssr/[root-of-the-server]__cbae9863._.js.map +0 -1
  362. package/web/.next/server/chunks/ssr/[root-of-the-server]__e5e09f72._.js.map +0 -1
  363. package/web/.next/server/chunks/ssr/_4469c284._.js +0 -6
  364. package/web/.next/server/chunks/ssr/_4469c284._.js.map +0 -1
  365. package/web/.next/server/chunks/ssr/_6e08ce21._.js +0 -3
  366. package/web/.next/server/chunks/ssr/_6e08ce21._.js.map +0 -1
  367. package/web/.next/server/chunks/ssr/src_presentation_web_0a49d9ee._.js +0 -8
  368. package/web/.next/server/chunks/ssr/src_presentation_web_0a49d9ee._.js.map +0 -1
  369. package/web/.next/server/chunks/ssr/src_presentation_web_79b0305a._.js +0 -8
  370. package/web/.next/server/chunks/ssr/src_presentation_web_79b0305a._.js.map +0 -1
  371. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_cc16bd01.js +0 -3
  372. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_cc16bd01.js.map +0 -1
  373. package/web/.next/server/chunks/ssr/src_presentation_web_app_(dashboard)_get-graph-data_ts_b018483e._.js +0 -3
  374. package/web/.next/server/chunks/ssr/src_presentation_web_app_(dashboard)_get-graph-data_ts_b018483e._.js.map +0 -1
  375. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_@drawer_repository_[repositoryId]_page_actions_3a02d8a5.js +0 -3
  376. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/8ba4b_server_app_(dashboard)_repository_[repositoryId]_page_actions_a3044852.js +0 -3
  377. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_4469c284._.js +0 -6
  378. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_6e08ce21._.js +0 -3
  379. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_0a49d9ee._.js +0 -8
  380. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_79b0305a._.js +0 -8
  381. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_cc16bd01.js +0 -3
  382. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_app_(dashboard)_get-graph-data_ts_b018483e._.js +0 -3
  383. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-node-config.ts +0 -8
  384. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-node.stories.tsx +0 -70
  385. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-node.tsx +0 -49
  386. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/index.ts +0 -3
  387. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/control-center-drawer.stories.tsx +0 -293
  388. package/web/.next/standalone/src/presentation/web/components/common/control-center-drawer/control-center-drawer.tsx +0 -745
  389. package/web/.next/standalone/src/presentation/web/components/common/notification-permission-banner/index.ts +0 -1
  390. package/web/.next/standalone/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.stories.tsx +0 -52
  391. package/web/.next/standalone/src/presentation/web/components/common/notification-permission-banner/notification-permission-banner.tsx +0 -55
  392. package/web/.next/static/chunks/073b31d7a4974504.js +0 -1
  393. package/web/.next/static/chunks/0c54314c8c9c9739.js +0 -7
  394. package/web/.next/static/chunks/0e588a3a4dd7d1c0.js +0 -1
  395. package/web/.next/static/chunks/2433994d844b7d39.js +0 -1
  396. package/web/.next/static/chunks/3e041a9008bac888.js +0 -1
  397. package/web/.next/static/chunks/798624a651d68855.js +0 -4
  398. package/web/.next/static/chunks/977b9d2b9ef19f7d.js +0 -1
  399. package/web/.next/static/chunks/9be617fc61b893f2.js +0 -1
  400. package/web/.next/static/chunks/a18b0561724d11d4.js +0 -1
  401. package/web/.next/static/chunks/acce8a15d6896dea.css +0 -2
  402. /package/dist/src/presentation/web/components/common/{add-repository-node → add-repository-button}/add-repository-button.d.ts +0 -0
  403. /package/dist/src/presentation/web/components/common/{add-repository-node → add-repository-button}/add-repository-button.js +0 -0
  404. /package/dist/src/presentation/web/components/common/{add-repository-node → add-repository-button}/pick-folder.d.ts +0 -0
  405. /package/dist/src/presentation/web/components/common/{add-repository-node → add-repository-button}/pick-folder.js +0 -0
  406. /package/web/.next/standalone/src/presentation/web/components/common/{add-repository-node → add-repository-button}/add-repository-button.tsx +0 -0
  407. /package/web/.next/standalone/src/presentation/web/components/common/{add-repository-node → add-repository-button}/pick-folder.ts +0 -0
  408. /package/web/.next/static/{pzwrJGWI__eUhJsxxW2S- → wSVxDvqh-ObIC1qlefqgl}/_buildManifest.js +0 -0
  409. /package/web/.next/static/{pzwrJGWI__eUhJsxxW2S- → wSVxDvqh-ObIC1qlefqgl}/_clientMiddlewareManifest.json +0 -0
  410. /package/web/.next/static/{pzwrJGWI__eUhJsxxW2S- → wSVxDvqh-ObIC1qlefqgl}/_ssgManifest.js +0 -0
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
5
  import { toast } from 'sonner';
6
- import { applyNodeChanges } from '@xyflow/react';
7
6
  import type { Connection, Edge, NodeChange } from '@xyflow/react';
8
7
  import type { FeatureNodeData } from '@/components/common/feature-node';
8
+ import type { RepositoryNodeData } from '@/components/common/repository-node';
9
9
  import type { CanvasNodeType } from '@/components/features/features-canvas';
10
10
  import {
11
11
  layoutWithDagre,
@@ -15,13 +15,15 @@ import {
15
15
  import { deleteFeature } from '@/app/actions/delete-feature';
16
16
  import { addRepository } from '@/app/actions/add-repository';
17
17
  import { deleteRepository } from '@/app/actions/delete-repository';
18
- import { fetchGraphData } from '@/app/actions/get-graph-data';
18
+ import { getFeatureMetadata } from '@/app/actions/get-feature-metadata';
19
19
  import { useAgentEventsContext } from '@/hooks/agent-events-provider';
20
20
  import { useSoundAction } from '@/hooks/use-sound-action';
21
21
  import {
22
22
  mapEventTypeToState,
23
- mapPhaseNameToLifecycle,
23
+ resolveSseEventUpdates,
24
24
  } from '@/components/common/feature-node/derive-feature-state';
25
+ import { useGraphState, type GraphCallbacks } from '@/hooks/use-graph-state';
26
+ import type { FeatureEntry } from '@/lib/derive-graph';
25
27
 
26
28
  export interface ControlCenterState {
27
29
  nodes: CanvasNodeType[];
@@ -37,8 +39,17 @@ export interface ControlCenterState {
37
39
  dataOverride?: Partial<FeatureNodeData>,
38
40
  edgeType?: string
39
41
  ) => string;
42
+ /** Stable lookup: repositoryPath for a feature node. */
43
+ getFeatureRepositoryPath: (featureNodeId: string) => string | undefined;
44
+ /** Stable lookup: repository data by nodeId. */
45
+ getRepositoryData: (nodeId: string) => RepositoryNodeData | undefined;
46
+ /** Sync callbacks into derived node data (does not trigger re-render). */
47
+ setCallbacks: (callbacks: GraphCallbacks) => void;
40
48
  }
41
49
 
50
+ /** Must match the message string emitted by the SSE route in agent-events/route.ts */
51
+ const METADATA_UPDATED_MESSAGE = 'Feature metadata updated';
52
+
42
53
  let nextFeatureId = 0;
43
54
 
44
55
  export function useControlCenterState(
@@ -46,123 +57,81 @@ export function useControlCenterState(
46
57
  initialEdges: Edge[]
47
58
  ): ControlCenterState {
48
59
  const router = useRouter();
49
- const [nodes, setNodes] = useState<CanvasNodeType[]>(initialNodes);
50
- // eslint-disable-next-line react/hook-use-state -- raw setter renamed; public setEdges wrapper keeps edgesRef in sync
51
- const [edges, setEdgesRaw] = useState<Edge[]>(initialEdges);
52
- const edgesRef = useRef<Edge[]>(initialEdges);
53
-
54
- // Wrapper that keeps edgesRef in sync with edges state, allowing
55
- // createFeatureNode to read current edges without a closure dependency.
56
- const setEdges = useCallback((update: Edge[] | ((prev: Edge[]) => Edge[])) => {
57
- setEdgesRaw((prev) => {
58
- const next = typeof update === 'function' ? update(prev) : update;
59
- edgesRef.current = next;
60
- return next;
61
- });
62
- }, []);
63
60
  const deleteSound = useSoundAction('delete');
64
61
  const createSound = useSoundAction('create');
65
62
 
66
- // Sync server props into local state when parent re-renders with new data
67
- const initialNodeKey = initialNodes
68
- .map((n) => n.id)
69
- .sort()
70
- .join(',');
71
- const initialEdgeKey = initialEdges
72
- .map((e) => e.id)
73
- .sort()
74
- .join(',');
75
-
76
- // Track previous feature states so we can detect transitions on server refresh
77
- const prevFeatureStatesRef = useRef<Map<string, FeatureNodeData['state']>>(new Map());
78
-
79
- // Stable key that changes when feature DATA changes (state/lifecycle), not just IDs.
80
- const initialDataKey = initialNodes
81
- .filter((n) => n.type === 'featureNode')
82
- .map((n) => {
83
- const d = n.data as FeatureNodeData;
84
- return `${n.id}:${d.state}:${d.lifecycle}`;
85
- })
86
- .sort()
87
- .join(',');
88
-
89
- // Sync server props into local state — keyed by derived strings (initialNodeKey/initialEdgeKey)
90
- // to avoid infinite re-renders from unstable array references.
63
+ const {
64
+ nodes,
65
+ edges,
66
+ reconcile,
67
+ updateFeature,
68
+ addPendingFeature,
69
+ removeFeature,
70
+ restoreFeature,
71
+ addRepository: addRepositoryToMap,
72
+ removeRepository,
73
+ replaceRepository,
74
+ getFeatureRepositoryPath,
75
+ getRepositoryData,
76
+ setCallbacks,
77
+ } = useGraphState(initialNodes, initialEdges);
78
+
79
+ // Refs for stable access to latest nodes/edges without callback recreation
80
+ const nodesRef = useRef(nodes);
81
+ const edgesRef = useRef(edges);
91
82
  useEffect(() => {
92
- setNodes((currentNodes) => {
93
- // Build a lookup of current node positions by ID
94
- const currentById = new Map(currentNodes.map((n) => [n.id, n]));
95
-
96
- // Identify optimistic "creating" nodes (they have temp IDs not in server data)
97
- const serverIds = new Set(initialNodes.map((n) => n.id));
98
- const creatingNodes = currentNodes.filter(
99
- (n) =>
100
- n.type === 'featureNode' &&
101
- (n.data as FeatureNodeData).state === 'creating' &&
102
- !serverIds.has(n.id)
103
- );
104
-
105
- // Merge server nodes with client positions, tracking if relayout is needed
106
- let needsRelayout = false;
107
- const merged = initialNodes.map((serverNode) => {
108
- // Node already exists on canvas — keep its position, update data
109
- const existing = currentById.get(serverNode.id);
110
- if (existing) {
111
- return { ...serverNode, position: existing.position };
112
- }
113
-
114
- // New server node — inherit position from an optimistic creating node if available
115
- if (serverNode.type === 'featureNode' && creatingNodes.length > 0) {
116
- const donor = creatingNodes.shift()!;
117
- return { ...serverNode, position: donor.position };
118
- }
119
-
120
- // Truly new node with no optimistic counterpart — needs relayout
121
- needsRelayout = true;
122
- return serverNode;
123
- });
124
-
125
- // Also check if non-creating nodes were removed (graph got smaller)
126
- if (!needsRelayout) {
127
- needsRelayout = currentNodes.some(
128
- (cn) =>
129
- !serverIds.has(cn.id) &&
130
- !(cn.type === 'featureNode' && (cn.data as FeatureNodeData).state === 'creating')
131
- );
132
- }
133
-
134
- if (needsRelayout) {
135
- const layoutResult = layoutWithDagre(merged, initialEdges, CANVAS_LAYOUT_DEFAULTS);
136
- setEdges(layoutResult.edges);
137
- return layoutResult.nodes;
138
- }
83
+ nodesRef.current = nodes;
84
+ edgesRef.current = edges;
85
+ }, [nodes, edges]);
86
+
87
+ // Sync server props into domain Maps when initialNodes/initialEdges change.
88
+ // Keyed by a stable string so we don't re-reconcile on every render.
89
+ const initialNodeKey = useMemo(
90
+ () =>
91
+ initialNodes
92
+ .map((n) => n.id)
93
+ .sort()
94
+ .join(','),
95
+ [initialNodes]
96
+ );
97
+ const initialDataKey = useMemo(
98
+ () =>
99
+ initialNodes
100
+ .filter((n) => n.type === 'featureNode')
101
+ .map((n) => {
102
+ const d = n.data as FeatureNodeData;
103
+ return `${n.id}:${d.state}:${d.lifecycle}`;
104
+ })
105
+ .sort()
106
+ .join(','),
107
+ [initialNodes]
108
+ );
139
109
 
140
- return merged;
141
- });
142
- }, [initialNodeKey, initialNodes, initialEdges, setEdges]);
110
+ const prevReconcileKey = useRef('');
143
111
 
144
112
  useEffect(() => {
145
- setEdges(initialEdges);
146
- }, [initialEdgeKey, initialEdges, setEdges]);
113
+ const key = `${initialNodeKey}|${initialDataKey}`;
114
+ if (key !== prevReconcileKey.current) {
115
+ prevReconcileKey.current = key;
116
+ reconcile(initialNodes, initialEdges);
117
+ }
118
+ }, [initialNodeKey, initialDataKey, initialNodes, initialEdges, reconcile]);
119
+
120
+ // Track previous feature states for fallback notifications
121
+ const prevFeatureStatesRef = useRef<Map<string, FeatureNodeData['state']>>(new Map());
147
122
 
148
- // Fallback notifications from server-prop state transitions.
149
- // Fires only when SSE didn't already deliver the matching event for a feature.
150
123
  const { events } = useAgentEventsContext();
151
124
 
152
125
  useEffect(() => {
153
126
  const prevStates = prevFeatureStatesRef.current;
154
-
155
127
  for (const node of initialNodes) {
156
128
  if (node.type !== 'featureNode') continue;
157
129
  const data = node.data as FeatureNodeData;
158
130
  const prev = prevStates.get(node.id);
159
-
160
131
  if (prev !== undefined && prev !== data.state) {
161
- // Check if SSE already delivered a matching event for this feature
162
132
  const sseAlreadyCovered = events.some(
163
133
  (e) => e.featureId === data.featureId && mapEventTypeToState(e.eventType) === data.state
164
134
  );
165
-
166
135
  if (!sseAlreadyCovered) {
167
136
  if (data.state === 'done') {
168
137
  toast.success(data.name, { description: 'Feature completed!' });
@@ -171,9 +140,7 @@ export function useControlCenterState(
171
140
  description: 'Waiting for your approval',
172
141
  action: {
173
142
  label: 'Review',
174
- onClick: () => {
175
- router.push(`/feature/${data.featureId}`);
176
- },
143
+ onClick: () => router.push(`/feature/${data.featureId}`),
177
144
  },
178
145
  });
179
146
  } else if (data.state === 'error') {
@@ -181,242 +148,83 @@ export function useControlCenterState(
181
148
  }
182
149
  }
183
150
  }
184
-
185
151
  prevStates.set(node.id, data.state);
186
152
  }
187
153
  }, [initialDataKey, initialNodes, events, router]);
188
154
 
189
- // Targeted optimistic updates from SSE agent events.
190
- // Uses `events` array (not `lastEvent`) so that React batching cannot silently
191
- // drop events when multiple SSE messages arrive in the same tick.
155
+ // SSE effect: only state + lifecycle updates
192
156
  const processedEventCountRef = useRef(0);
193
157
 
194
- // Track known feature IDs so we can detect when SSE reports features not yet on the canvas.
195
- // Initialised from initialNodes to avoid a race between first render and SSE arrival.
196
- const knownFeatureIdsRef = useRef<Set<string>>(
197
- new Set(
198
- initialNodes
199
- .filter((n) => n.type === 'featureNode')
200
- .map((n) => (n.data as FeatureNodeData).featureId)
201
- )
202
- );
203
- // Keep ref in sync as nodes change (new nodes added, temp nodes replaced, etc.)
204
- useEffect(() => {
205
- knownFeatureIdsRef.current = new Set(
206
- nodes
207
- .filter((n) => n.type === 'featureNode')
208
- .map((n) => (n.data as FeatureNodeData).featureId)
209
- );
210
- }, [nodes]);
211
-
212
- // Guard against concurrent fetchGraphData calls triggered by unknown-feature detection
213
- const fetchingGraphRef = useRef(false);
214
-
215
158
  useEffect(() => {
159
+ // Clamp cursor if events were pruned
160
+ if (processedEventCountRef.current > events.length) {
161
+ processedEventCountRef.current = 0;
162
+ }
216
163
  if (events.length <= processedEventCountRef.current) return;
217
-
218
164
  const newEvents = events.slice(processedEventCountRef.current);
219
165
  processedEventCountRef.current = events.length;
220
166
 
221
- let hasUnknownFeature = false;
222
-
223
- for (const event of newEvents) {
224
- // If the feature isn't on the canvas yet, flag for a full graph refresh
225
- if (!knownFeatureIdsRef.current.has(event.featureId)) {
226
- hasUnknownFeature = true;
227
- continue;
228
- }
229
-
230
- const newState = mapEventTypeToState(event.eventType);
231
- const newLifecycle = mapPhaseNameToLifecycle(event.phaseName);
232
-
233
- // Targeted node update — only clone the matched node
234
- setNodes((prev) =>
235
- prev.map((node) => {
236
- if (node.type !== 'featureNode') return node;
237
- const data = node.data as FeatureNodeData;
238
- if (data.featureId !== event.featureId) return node;
239
-
240
- return {
241
- ...node,
242
- data: {
243
- ...data,
244
- state: newState,
245
- ...(newLifecycle !== undefined && { lifecycle: newLifecycle }),
246
- },
247
- };
248
- })
249
- );
250
- }
251
-
252
- // SSE reported features not on the canvas — fetch full graph data so new
253
- // repository nodes, feature nodes, and edges all appear together.
254
- if (hasUnknownFeature && !fetchingGraphRef.current) {
255
- fetchingGraphRef.current = true;
256
- fetchGraphData()
257
- .then(({ nodes: serverNodes, edges: serverEdges }) => {
258
- setNodes((currentNodes) => {
259
- // Preserve positions of nodes already on the canvas
260
- const currentById = new Map(currentNodes.map((n) => [n.id, n]));
261
- const merged = serverNodes.map((serverNode) => {
262
- const existing = currentById.get(serverNode.id);
263
- return existing ? { ...serverNode, position: existing.position } : serverNode;
264
- });
265
-
266
- const layoutResult = layoutWithDagre(merged, serverEdges, CANVAS_LAYOUT_DEFAULTS);
267
- setEdges(layoutResult.edges);
268
- return layoutResult.nodes;
269
- });
270
- })
271
- .catch(() => {
272
- // Silently ignore — next SSE event or poll will retry
273
- })
274
- .finally(() => {
275
- fetchingGraphRef.current = false;
167
+ for (const { featureId, state, lifecycle } of resolveSseEventUpdates(newEvents)) {
168
+ if (state !== undefined || lifecycle !== undefined) {
169
+ updateFeature(`feat-${featureId}`, {
170
+ ...(state !== undefined && { state }),
171
+ ...(lifecycle !== undefined && { lifecycle }),
276
172
  });
173
+ }
277
174
  }
278
- }, [events, setEdges]);
279
-
280
- // Lightweight server-action polling fallback: fetches fresh graph data every 5s
281
- // when any feature is in an active state. This avoids full router.refresh() while
282
- // ensuring the UI stays in sync even if SSE events are missed.
283
- //
284
- // We derive a stable boolean so the interval isn't torn down on every node change.
285
- const hasActiveFeature = nodes.some((n) => {
286
- if (n.type !== 'featureNode') return false;
287
- const data = n.data as FeatureNodeData;
288
- return (
289
- data.state === 'running' || data.state === 'action-required' || data.state === 'creating'
290
- );
291
- });
175
+ }, [events, updateFeature]);
292
176
 
177
+ // Listen for optimistic approval events from the drawer (fires before SSE arrives)
293
178
  useEffect(() => {
294
- if (!hasActiveFeature) return;
179
+ const handler = (e: Event) => {
180
+ const { featureId } = (e as CustomEvent<{ featureId: string }>).detail;
181
+ updateFeature(`feat-${featureId}`, { state: 'running' });
182
+ };
183
+ window.addEventListener('shep:feature-approved', handler);
184
+ return () => window.removeEventListener('shep:feature-approved', handler);
185
+ }, [updateFeature]);
186
+
187
+ // Separate effect: fetch metadata (name + description) when SSE reports it changed
188
+ const metadataFetchedRef = useRef<Set<string>>(new Set());
189
+ const processedMetadataCountRef = useRef(0);
295
190
 
296
- const interval = setInterval(async () => {
297
- try {
298
- const { nodes: serverNodes } = await fetchGraphData();
299
- setNodes((currentNodes) => {
300
- const serverById = new Map(serverNodes.map((n) => [n.id, n]));
301
- let changed = false;
302
-
303
- const merged = currentNodes.map((node) => {
304
- const serverNode = serverById.get(node.id);
305
- if (!serverNode || node.type !== 'featureNode') return node;
306
- const currentData = node.data as FeatureNodeData;
307
- const serverData = serverNode.data as FeatureNodeData;
308
-
309
- // Only update if server data has different state or lifecycle
310
- if (
311
- currentData.state === serverData.state &&
312
- currentData.lifecycle === serverData.lifecycle
313
- ) {
314
- return node;
315
- }
316
-
317
- changed = true;
318
- return {
319
- ...node,
320
- data: {
321
- ...currentData,
322
- state: serverData.state,
323
- lifecycle: serverData.lifecycle,
324
- progress: serverData.progress,
325
- ...(serverData.errorMessage && { errorMessage: serverData.errorMessage }),
326
- ...(serverData.runtime && { runtime: serverData.runtime }),
327
- ...(serverData.agentType && { agentType: serverData.agentType }),
328
- ...(serverData.pr && { pr: serverData.pr }),
329
- ...(serverData.blockedBy && { blockedBy: serverData.blockedBy }),
330
- },
331
- };
332
- });
333
-
334
- // Find new server nodes not yet on the client
335
- const clientIds = new Set(currentNodes.map((n) => n.id));
336
- const newServerNodes = serverNodes.filter((n) => !clientIds.has(n.id));
337
-
338
- // Replace optimistic "creating" temp nodes with their real server
339
- // counterparts. Temp nodes have IDs like "feature-<ts>" while real
340
- // nodes use "feat-<uuid>". Match by repositoryPath + name.
341
- if (newServerNodes.length > 0) {
342
- const creatingIndices: number[] = [];
343
- merged.forEach((n, i) => {
344
- if (n.type === 'featureNode' && (n.data as FeatureNodeData).state === 'creating') {
345
- creatingIndices.push(i);
346
- }
347
- });
191
+ useEffect(() => {
192
+ // Clamp cursor if events were pruned
193
+ if (processedMetadataCountRef.current > events.length) {
194
+ processedMetadataCountRef.current = 0;
195
+ }
196
+ if (events.length <= processedMetadataCountRef.current) return;
197
+ const newEvents = events.slice(processedMetadataCountRef.current);
198
+ processedMetadataCountRef.current = events.length;
348
199
 
349
- for (const serverNode of newServerNodes) {
350
- if (serverNode.type !== 'featureNode' || creatingIndices.length === 0) continue;
351
- const idx = creatingIndices.shift()!;
352
- // Replace the temp node in-place, keeping its position
353
- merged[idx] = { ...serverNode, position: merged[idx].position };
354
- changed = true;
355
- }
356
-
357
- // Any remaining new server nodes that didn't replace a temp node
358
- const replacedIds = new Set(merged.map((n) => n.id));
359
- const trueNewNodes = newServerNodes.filter((n) => !replacedIds.has(n.id));
360
- if (trueNewNodes.length > 0) {
361
- changed = true;
362
- return [...merged, ...trueNewNodes];
363
- }
200
+ for (const event of newEvents) {
201
+ if (event.message !== METADATA_UPDATED_MESSAGE) continue;
202
+ if (metadataFetchedRef.current.has(event.featureId)) continue;
203
+ metadataFetchedRef.current.add(event.featureId);
204
+
205
+ const nodeId = `feat-${event.featureId}`;
206
+ getFeatureMetadata(event.featureId)
207
+ .then((meta) => {
208
+ if (meta) {
209
+ updateFeature(nodeId, { name: meta.name, description: meta.description });
364
210
  }
365
-
366
- return changed ? merged : currentNodes;
211
+ })
212
+ .catch(() => {
213
+ // Silent: metadata fetch failure is non-critical
367
214
  });
368
- } catch {
369
- // Silently ignore polling errors — SSE is the primary mechanism
370
- }
371
- }, 5_000);
372
-
373
- return () => clearInterval(interval);
374
- }, [hasActiveFeature]);
375
-
376
- const onNodesChange = useCallback((changes: NodeChange<CanvasNodeType>[]) => {
377
- // Filter out 'replace' changes — in controlled mode these come from
378
- // React Flow's StoreUpdater syncing the nodes prop back into state.
379
- // Applying them creates a circular update loop (nodes → enrichedNodes →
380
- // StoreUpdater → onNodesChange → setNodes → nodes → …) that both causes
381
- // excessive re-renders and overwrites SSE-driven state changes with stale data.
382
- const applicable = changes.filter((c) => c.type !== 'replace');
383
- if (applicable.length > 0) {
384
- setNodes((ns) => applyNodeChanges(applicable, ns));
385
215
  }
386
- }, []);
387
-
388
- const handleConnect = useCallback(
389
- (connection: Connection) => {
390
- if (!connection.source || !connection.target) return;
216
+ }, [events, updateFeature]);
391
217
 
392
- setNodes((currentNodes) => {
393
- const sourceNode = currentNodes.find((n) => n.id === connection.source);
394
- if (sourceNode?.type !== 'repositoryNode') return currentNodes;
395
-
396
- // Block if target feature already has a repo source
397
- setEdges((currentEdges) => {
398
- const targetAlreadyHasRepo = currentEdges.some((e) => {
399
- const edgeSource = currentNodes.find((n) => n.id === e.source);
400
- return edgeSource?.type === 'repositoryNode' && e.target === connection.target;
401
- });
402
- if (targetAlreadyHasRepo) return currentEdges;
403
-
404
- return [
405
- ...currentEdges,
406
- {
407
- id: `edge-${connection.source}-${connection.target}`,
408
- source: connection.source,
409
- target: connection.target,
410
- style: { strokeDasharray: '5 5' },
411
- },
412
- ];
413
- });
218
+ // onNodesChange is a no-op: nodes are derived from domain Maps.
219
+ // Since nodesDraggable=false and elementsSelectable=false, only React Flow's
220
+ // internal replace/dimensions changes come through — we ignore them all.
221
+ const onNodesChange = useCallback((_changes: NodeChange<CanvasNodeType>[]) => {
222
+ // Intentional no-op: domain Maps are the source of truth.
223
+ }, []);
414
224
 
415
- return currentNodes;
416
- });
417
- },
418
- [setEdges]
419
- );
225
+ const handleConnect = useCallback((_connection: Connection) => {
226
+ // Connections are managed via domain operations, not direct edge manipulation.
227
+ }, []);
420
228
 
421
229
  const createFeatureNode = useCallback(
422
230
  (
@@ -424,11 +232,14 @@ export function useControlCenterState(
424
232
  dataOverride?: Partial<FeatureNodeData>,
425
233
  edgeType?: string
426
234
  ): string => {
427
- const id = `feature-${Date.now()}-${nextFeatureId++}`;
235
+ // Use real feature ID when available (from server), otherwise temp ID
236
+ const id = dataOverride?.featureId
237
+ ? `feat-${dataOverride.featureId}`
238
+ : `feature-${Date.now()}-${nextFeatureId++}`;
428
239
  const newFeatureData: FeatureNodeData = {
429
240
  name: dataOverride?.name ?? 'New Feature',
430
241
  description: dataOverride?.description ?? 'Describe what this feature does',
431
- featureId: `#${id.slice(-4)}`,
242
+ featureId: dataOverride?.featureId ?? `#${id.slice(-4)}`,
432
243
  lifecycle: 'requirements',
433
244
  state: dataOverride?.state ?? 'running',
434
245
  progress: 0,
@@ -436,171 +247,115 @@ export function useControlCenterState(
436
247
  branch: dataOverride?.branch ?? '',
437
248
  };
438
249
 
439
- setNodes((currentNodes) => {
440
- const newNode = {
441
- id,
442
- type: 'featureNode' as const,
443
- position: { x: 0, y: 0 },
444
- data: newFeatureData,
445
- } as CanvasNodeType;
446
-
447
- const newEdge = sourceNodeId
448
- ? {
449
- id:
450
- edgeType === 'dependencyEdge'
451
- ? `dep-${sourceNodeId}-${id}`
452
- : `edge-${sourceNodeId}-${id}`,
453
- source: sourceNodeId,
454
- target: id,
455
- ...(edgeType ? { type: edgeType } : { style: { strokeDasharray: '5 5' } }),
456
- }
457
- : null;
458
-
459
- const allEdges = newEdge
460
- ? [...edgesRef.current.filter((e) => e.id !== newEdge.id), newEdge]
461
- : edgesRef.current;
462
-
463
- // Insert the new node after the last sibling connected to the same source,
464
- // so dagre's input-order preservation keeps repo groups together.
465
- let insertIndex = currentNodes.length;
466
- if (sourceNodeId) {
467
- const siblingIds = new Set(
468
- allEdges.filter((e) => e.source === sourceNodeId).map((e) => e.target)
469
- );
470
- for (let i = currentNodes.length - 1; i >= 0; i--) {
471
- if (siblingIds.has(currentNodes[i].id)) {
472
- insertIndex = i + 1;
473
- break;
474
- }
475
- }
476
- }
477
- const allNodes = [
478
- ...currentNodes.slice(0, insertIndex),
479
- newNode,
480
- ...currentNodes.slice(insertIndex),
481
- ];
482
-
483
- // Run dagre layout on the full graph so the new node is positioned consistently
484
- const layoutResult = layoutWithDagre(allNodes, allEdges, CANVAS_LAYOUT_DEFAULTS);
485
- setEdges(layoutResult.edges);
486
- return layoutResult.nodes;
487
- });
250
+ const parentNodeId = edgeType === 'dependencyEdge' && sourceNodeId ? sourceNodeId : undefined;
251
+
252
+ addPendingFeature(id, newFeatureData, parentNodeId);
488
253
 
489
254
  return id;
490
255
  },
491
- [setEdges]
256
+ [addPendingFeature]
492
257
  );
493
258
 
494
259
  const handleDeleteFeature = useCallback(
495
260
  (featureId: string) => {
496
261
  const nodeId = `feat-${featureId}`;
497
262
 
498
- // Snapshot current state for rollback
499
- const prevNodes = nodes;
500
- const prevEdges = edgesRef.current;
263
+ // Find the current entry for rollback
264
+ const prevEntry = nodesRef.current
265
+ .filter((n) => n.id === nodeId)
266
+ .map((n) => ({
267
+ nodeId: n.id,
268
+ data: n.data as FeatureNodeData,
269
+ }))[0];
501
270
 
502
- // Optimistic removal — update UI immediately
503
- setNodes((currentNodes) => {
504
- const remainingNodes = currentNodes.filter((n) => n.id !== nodeId);
505
- const remainingEdges = edgesRef.current.filter(
506
- (e) => e.source !== nodeId && e.target !== nodeId
507
- );
508
- const layoutResult = layoutWithDagre(
509
- remainingNodes,
510
- remainingEdges,
511
- CANVAS_LAYOUT_DEFAULTS
512
- );
513
- setEdges(layoutResult.edges);
514
- return layoutResult.nodes;
515
- });
271
+ // Optimistic removal
272
+ removeFeature(nodeId);
516
273
  deleteSound.play();
517
274
  toast.success('Feature deleted successfully');
518
275
  router.push('/');
519
276
 
520
- // Persist in background — rollback on failure
521
277
  deleteFeature(featureId)
522
278
  .then((result) => {
523
279
  if (result.error) {
524
- setNodes(prevNodes);
525
- setEdges(prevEdges);
280
+ if (prevEntry) restoreFeature(nodeId, prevEntry);
526
281
  toast.error(result.error);
527
282
  }
528
283
  })
529
284
  .catch(() => {
530
- setNodes(prevNodes);
531
- setEdges(prevEdges);
285
+ if (prevEntry) restoreFeature(nodeId, prevEntry);
532
286
  toast.error('Failed to delete feature');
533
287
  });
534
288
  },
535
- [nodes, router, deleteSound, setEdges]
289
+ [router, deleteSound, removeFeature, restoreFeature]
536
290
  );
537
291
 
538
292
  const handleDeleteRepository = useCallback(
539
293
  async (repositoryId: string) => {
540
294
  const repoNodeId = `repo-${repositoryId}`;
541
295
 
296
+ // Find children of this repo via edges
297
+ const childFeatureIds = new Set(
298
+ edgesRef.current.filter((e) => e.source === repoNodeId).map((e) => e.target)
299
+ );
300
+
542
301
  // Snapshot for rollback
543
- const prevNodes = nodes;
544
- const prevEdges = edgesRef.current;
545
-
546
- // Optimistic: remove repo node, its child feature nodes, and all related edges
547
- setNodes((currentNodes) => {
548
- // Find feature node IDs connected to this repo via edges
549
- const childFeatureIds = new Set(
550
- edgesRef.current.filter((e) => e.source === repoNodeId).map((e) => e.target)
551
- );
552
- const remainingNodes = currentNodes.filter(
553
- (n) => n.id !== repoNodeId && !childFeatureIds.has(n.id)
554
- );
555
- const remainingEdges = edgesRef.current.filter(
556
- (e) =>
557
- e.source !== repoNodeId &&
558
- e.target !== repoNodeId &&
559
- !childFeatureIds.has(e.source) &&
560
- !childFeatureIds.has(e.target)
561
- );
562
- const layoutResult = layoutWithDagre(
563
- remainingNodes,
564
- remainingEdges,
565
- CANVAS_LAYOUT_DEFAULTS
566
- );
567
- setEdges(layoutResult.edges);
568
- return layoutResult.nodes;
569
- });
302
+ const prevRepoData = getRepositoryData(repoNodeId);
303
+ const childSnapshots = new Map<string, FeatureEntry>();
304
+ for (const childId of childFeatureIds) {
305
+ const childNode = nodesRef.current.find((n) => n.id === childId);
306
+ if (childNode) {
307
+ childSnapshots.set(childId, { nodeId: childId, data: childNode.data as FeatureNodeData });
308
+ }
309
+ }
310
+
311
+ const rollback = () => {
312
+ if (prevRepoData) addRepositoryToMap(repoNodeId, prevRepoData);
313
+ for (const [childId, entry] of childSnapshots) {
314
+ restoreFeature(childId, entry);
315
+ }
316
+ };
317
+
318
+ // Optimistic: remove repo + children
319
+ removeRepository(repoNodeId);
320
+ for (const childId of childFeatureIds) {
321
+ removeFeature(childId);
322
+ }
570
323
 
571
324
  try {
572
325
  const result = await deleteRepository(repositoryId);
573
-
574
326
  if (!result.success) {
575
327
  toast.error(result.error ?? 'Failed to remove repository');
576
- setNodes(prevNodes);
577
- setEdges(prevEdges);
328
+ rollback();
578
329
  return;
579
330
  }
580
-
581
331
  deleteSound.play();
582
332
  toast.success('Repository removed');
583
333
  } catch {
584
334
  toast.error('Failed to remove repository');
585
- setNodes(prevNodes);
586
- setEdges(prevEdges);
335
+ rollback();
587
336
  }
588
337
  },
589
- [nodes, deleteSound, setEdges]
338
+ [
339
+ deleteSound,
340
+ removeRepository,
341
+ removeFeature,
342
+ addRepositoryToMap,
343
+ restoreFeature,
344
+ getRepositoryData,
345
+ ]
590
346
  );
591
347
 
592
348
  const handleLayout = useCallback(
593
349
  (direction: LayoutDirection) => {
594
- setNodes((currentNodes) => {
595
- const result = layoutWithDagre(currentNodes, edgesRef.current, {
596
- ...CANVAS_LAYOUT_DEFAULTS,
597
- direction,
598
- });
599
- setEdges(result.edges);
600
- return result.nodes;
350
+ // Layout is applied via reconcile on next server prop update.
351
+ // For immediate re-layout, we apply dagre and trigger a reconcile-like update.
352
+ const result = layoutWithDagre(nodesRef.current, edgesRef.current, {
353
+ ...CANVAS_LAYOUT_DEFAULTS,
354
+ direction,
601
355
  });
356
+ reconcile(result.nodes, result.edges);
602
357
  },
603
- [setEdges]
358
+ [reconcile]
604
359
  );
605
360
 
606
361
  const handleAddRepository = useCallback(
@@ -612,81 +367,30 @@ export function useControlCenterState(
612
367
  .split(/[\\/]/)
613
368
  .pop() ?? path;
614
369
 
615
- // Optimistic UI: add node and re-layout immediately
616
- setNodes((currentNodes) => {
617
- const newNode = {
618
- id: tempId,
619
- type: 'repositoryNode' as const,
620
- position: { x: 0, y: 0 },
621
- data: { name: repoName, repositoryPath: path, id: tempId },
622
- } as CanvasNodeType;
623
-
624
- const allNodes = [...currentNodes, newNode];
625
- const layoutResult = layoutWithDagre(allNodes, edgesRef.current, CANVAS_LAYOUT_DEFAULTS);
626
- setEdges(layoutResult.edges);
627
- return layoutResult.nodes;
628
- });
370
+ addRepositoryToMap(tempId, { name: repoName, repositoryPath: path, id: tempId });
629
371
 
630
- // Persist via server action
631
372
  addRepository({ path, name: repoName })
632
373
  .then((result) => {
633
374
  if (result.error) {
634
- // Rollback optimistic node and re-layout
635
- setNodes((prev) => {
636
- const remaining = prev.filter((n) => n.id !== tempId);
637
- const layoutResult = layoutWithDagre(
638
- remaining,
639
- edgesRef.current,
640
- CANVAS_LAYOUT_DEFAULTS
641
- );
642
- setEdges(layoutResult.edges);
643
- return layoutResult.nodes;
644
- });
375
+ removeRepository(tempId);
645
376
  toast.error(result.error);
646
377
  return;
647
378
  }
648
-
649
- // Replace temp ID with real repository ID
650
379
  const repo = result.repository!;
651
380
  const realId = `repo-${repo.id}`;
652
- setNodes((prev) =>
653
- prev.map((n) =>
654
- n.id === tempId
655
- ? ({
656
- ...n,
657
- id: realId,
658
- data: { ...n.data, id: repo.id, repositoryPath: repo.path },
659
- } as CanvasNodeType)
660
- : n
661
- )
662
- );
663
- setEdges((prev) =>
664
- prev.map((e) => ({
665
- ...e,
666
- source: e.source === tempId ? realId : e.source,
667
- target: e.target === tempId ? realId : e.target,
668
- id: e.id.replace(tempId, realId),
669
- }))
670
- );
671
-
381
+ replaceRepository(tempId, realId, {
382
+ name: repo.name,
383
+ repositoryPath: repo.path,
384
+ id: repo.id,
385
+ });
672
386
  createSound.play();
673
387
  })
674
388
  .catch(() => {
675
- // Rollback optimistic node and re-layout
676
- setNodes((prev) => {
677
- const remaining = prev.filter((n) => n.id !== tempId);
678
- const layoutResult = layoutWithDagre(
679
- remaining,
680
- edgesRef.current,
681
- CANVAS_LAYOUT_DEFAULTS
682
- );
683
- setEdges(layoutResult.edges);
684
- return layoutResult.nodes;
685
- });
389
+ removeRepository(tempId);
686
390
  toast.error('Failed to add repository');
687
391
  });
688
392
  },
689
- [createSound, setEdges]
393
+ [addRepositoryToMap, removeRepository, replaceRepository, createSound]
690
394
  );
691
395
 
692
396
  return {
@@ -699,5 +403,8 @@ export function useControlCenterState(
699
403
  handleDeleteFeature,
700
404
  handleDeleteRepository,
701
405
  createFeatureNode,
406
+ getFeatureRepositoryPath,
407
+ getRepositoryData,
408
+ setCallbacks,
702
409
  };
703
410
  }