@shepai/cli 1.52.2 → 1.53.1

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 (186) hide show
  1. package/apis/json-schema/CiFixRecord.yaml +24 -0
  2. package/apis/json-schema/Feature.yaml +3 -0
  3. package/apis/json-schema/PullRequest.yaml +10 -0
  4. package/apis/json-schema/SdlcLifecycle.yaml +1 -0
  5. package/apis/json-schema/WorkflowConfig.yaml +15 -0
  6. package/dist/packages/core/src/application/ports/output/repositories/feature-repository.interface.d.ts +8 -0
  7. package/dist/packages/core/src/application/ports/output/repositories/feature-repository.interface.d.ts.map +1 -1
  8. package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts +12 -0
  9. package/dist/packages/core/src/application/ports/output/services/git-pr-service.interface.d.ts.map +1 -1
  10. package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.d.ts +30 -0
  11. package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.d.ts.map +1 -0
  12. package/dist/packages/core/src/application/use-cases/features/check-and-unblock-features.use-case.js +74 -0
  13. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.d.ts.map +1 -1
  14. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.js +60 -16
  15. package/dist/packages/core/src/application/use-cases/features/create/types.d.ts +2 -0
  16. package/dist/packages/core/src/application/use-cases/features/create/types.d.ts.map +1 -1
  17. package/dist/packages/core/src/application/use-cases/features/delete-feature.use-case.d.ts.map +1 -1
  18. package/dist/packages/core/src/application/use-cases/features/delete-feature.use-case.js +11 -4
  19. package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.d.ts +28 -0
  20. package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.d.ts.map +1 -0
  21. package/dist/packages/core/src/application/use-cases/features/update/update-feature-lifecycle.use-case.js +53 -0
  22. package/dist/packages/core/src/domain/generated/output.d.ts +47 -1
  23. package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
  24. package/dist/packages/core/src/domain/generated/output.js +1 -0
  25. package/dist/packages/core/src/domain/lifecycle-gates.d.ts +16 -0
  26. package/dist/packages/core/src/domain/lifecycle-gates.d.ts.map +1 -0
  27. package/dist/packages/core/src/domain/lifecycle-gates.js +19 -0
  28. package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
  29. package/dist/packages/core/src/infrastructure/di/container.js +12 -0
  30. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.d.ts +3 -0
  31. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.d.ts.map +1 -1
  32. package/dist/packages/core/src/infrastructure/persistence/sqlite/mappers/feature.mapper.js +8 -0
  33. package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
  34. package/dist/packages/core/src/infrastructure/persistence/sqlite/migrations.js +28 -1
  35. package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.d.ts +1 -0
  36. package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.d.ts.map +1 -1
  37. package/dist/packages/core/src/infrastructure/repositories/sqlite-feature.repository.js +12 -0
  38. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts +52 -1
  39. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts.map +1 -1
  40. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.js +1 -1
  41. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
  42. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +7 -2
  43. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.d.ts +27 -5
  44. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.d.ts.map +1 -1
  45. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/lifecycle-context.js +19 -16
  46. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.d.ts +24 -0
  47. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.d.ts.map +1 -0
  48. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-helpers.js +47 -0
  49. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.d.ts +43 -0
  50. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.d.ts.map +1 -0
  51. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/ci-watch-fix-loop.js +118 -0
  52. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge-output-parser.d.ts.map +1 -0
  53. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge.node.d.ts → merge/merge.node.d.ts} +10 -4
  54. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge/merge.node.d.ts.map +1 -0
  55. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge.node.js → merge/merge.node.js} +44 -17
  56. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.d.ts +12 -0
  57. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.d.ts.map +1 -1
  58. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/merge-prompts.js +38 -0
  59. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts +4 -1
  60. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts.map +1 -1
  61. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.js +13 -0
  62. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts +2 -0
  63. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.d.ts.map +1 -1
  64. package/dist/packages/core/src/infrastructure/services/git/git-pr.service.js +29 -1
  65. package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
  66. package/dist/src/presentation/cli/commands/feat/ls.command.js +40 -3
  67. package/dist/src/presentation/cli/commands/feat/new.command.d.ts.map +1 -1
  68. package/dist/src/presentation/cli/commands/feat/new.command.js +18 -0
  69. package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
  70. package/dist/src/presentation/cli/commands/feat/show.command.js +20 -0
  71. package/dist/src/presentation/web/app/actions/create-feature.d.ts +1 -0
  72. package/dist/src/presentation/web/app/actions/create-feature.d.ts.map +1 -1
  73. package/dist/src/presentation/web/app/actions/create-feature.js +2 -1
  74. package/dist/src/presentation/web/app/page.js +34 -33
  75. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.d.ts +6 -0
  76. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.d.ts.map +1 -0
  77. package/dist/src/presentation/web/components/common/add-repository-node/add-repository-button.js +23 -0
  78. package/dist/src/presentation/web/components/common/add-repository-node/index.d.ts +1 -0
  79. package/dist/src/presentation/web/components/common/add-repository-node/index.d.ts.map +1 -1
  80. package/dist/src/presentation/web/components/common/add-repository-node/index.js +1 -0
  81. package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.d.ts +11 -1
  82. package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.d.ts.map +1 -1
  83. package/dist/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.js +52 -5
  84. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.d.ts.map +1 -1
  85. package/dist/src/presentation/web/components/common/feature-node/derive-feature-state.js +4 -0
  86. package/dist/src/presentation/web/components/common/feature-node/feature-node.js +1 -1
  87. package/dist/src/presentation/web/components/features/control-center/control-center-inner.d.ts.map +1 -1
  88. package/dist/src/presentation/web/components/features/control-center/control-center-inner.js +13 -4
  89. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts +2 -0
  90. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.d.ts.map +1 -1
  91. package/dist/src/presentation/web/components/features/control-center/use-control-center-state.js +26 -8
  92. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.d.ts +7 -0
  93. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.d.ts.map +1 -0
  94. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.js +20 -0
  95. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.d.ts +6 -0
  96. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.d.ts.map +1 -0
  97. package/dist/src/presentation/web/components/features/features-canvas/dependency-edge.stories.js +58 -0
  98. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.d.ts.map +1 -1
  99. package/dist/src/presentation/web/components/features/features-canvas/features-canvas.js +7 -3
  100. package/dist/tsconfig.build.tsbuildinfo +1 -1
  101. package/package.json +1 -1
  102. package/web/.next/BUILD_ID +1 -1
  103. package/web/.next/build-manifest.json +2 -2
  104. package/web/.next/cache/.previewinfo +1 -1
  105. package/web/.next/cache/.rscinfo +1 -1
  106. package/web/.next/cache/.tsbuildinfo +1 -1
  107. package/web/.next/cache/config.json +3 -3
  108. package/web/.next/fallback-build-manifest.json +2 -2
  109. package/web/.next/prerender-manifest.json +3 -3
  110. package/web/.next/required-server-files.js +1 -1
  111. package/web/.next/required-server-files.json +1 -1
  112. package/web/.next/server/app/_global-error.html +2 -2
  113. package/web/.next/server/app/_global-error.rsc +1 -1
  114. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  115. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  116. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  117. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  118. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  119. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  120. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  121. package/web/.next/server/app/page.js.nft.json +1 -1
  122. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  123. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  124. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  125. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  126. package/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js +1 -1
  127. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  128. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
  129. package/web/.next/server/chunks/ssr/_1fafa9db._.js +1 -1
  130. package/web/.next/server/chunks/ssr/_1fafa9db._.js.map +1 -1
  131. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  132. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  133. package/web/.next/server/pages/500.html +2 -2
  134. package/web/.next/server/server-reference-manifest.js +1 -1
  135. package/web/.next/server/server-reference-manifest.json +15 -15
  136. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  137. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  138. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  139. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  140. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  141. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  142. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  143. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  144. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  145. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  146. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  147. package/web/.next/standalone/src/presentation/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  148. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  149. package/web/.next/standalone/src/presentation/web/.next/server/app/page.js.nft.json +1 -1
  150. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  151. package/web/.next/standalone/src/presentation/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  152. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  153. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__249c74f6._.js +1 -1
  154. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  155. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/_1fafa9db._.js +1 -1
  156. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js +1 -1
  157. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  158. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  159. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  160. package/web/.next/standalone/src/presentation/web/app/actions/create-feature.ts +4 -1
  161. package/web/.next/standalone/src/presentation/web/app/page.tsx +36 -36
  162. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/add-repository-button.tsx +43 -0
  163. package/web/.next/standalone/src/presentation/web/components/common/add-repository-node/index.ts +1 -0
  164. package/web/.next/standalone/src/presentation/web/components/common/feature-create-drawer/feature-create-drawer.tsx +199 -2
  165. package/web/.next/standalone/src/presentation/web/components/common/feature-node/derive-feature-state.ts +5 -0
  166. package/web/.next/standalone/src/presentation/web/components/common/feature-node/feature-node.tsx +1 -1
  167. package/web/.next/standalone/src/presentation/web/components/features/control-center/control-center-inner.tsx +20 -1
  168. package/web/.next/standalone/src/presentation/web/components/features/control-center/use-control-center-state.ts +43 -12
  169. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/dependency-edge.stories.tsx +81 -0
  170. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/dependency-edge.tsx +29 -0
  171. package/web/.next/standalone/src/presentation/web/components/features/features-canvas/features-canvas.tsx +16 -2
  172. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  173. package/web/.next/static/chunks/212e3c682d3b7f50.js +10 -0
  174. package/web/.next/static/chunks/476fe030d242c153.css +2 -0
  175. package/web/.next/static/chunks/{31e5030eed01a941.js → 68fb91b6eca14536.js} +1 -1
  176. package/web/.next/trace +1 -1
  177. package/web/.next/trace-build +1 -1
  178. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge-output-parser.d.ts.map +0 -1
  179. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.d.ts.map +0 -1
  180. package/web/.next/static/chunks/23cae32e6f42833d.js +0 -10
  181. package/web/.next/static/chunks/59b8bd47023e0249.css +0 -2
  182. /package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge-output-parser.d.ts → merge/merge-output-parser.d.ts} +0 -0
  183. /package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/{merge-output-parser.js → merge/merge-output-parser.js} +0 -0
  184. /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → EWkET8BUeCcZJxcYQpxDY}/_buildManifest.js +0 -0
  185. /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → EWkET8BUeCcZJxcYQpxDY}/_clientMiddlewareManifest.json +0 -0
  186. /package/web/.next/static/{-c2lwSbfsfZHNnfB5CogT → EWkET8BUeCcZJxcYQpxDY}/_ssgManifest.js +0 -0
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Plus, Loader2 } from 'lucide-react';
5
+ import { pickFolder } from './pick-folder';
6
+
7
+ interface AddRepositoryButtonProps {
8
+ onSelect?: (path: string) => void;
9
+ }
10
+
11
+ export function AddRepositoryButton({ onSelect }: AddRepositoryButtonProps) {
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ async function handleClick() {
15
+ if (loading) return;
16
+ setLoading(true);
17
+ try {
18
+ const path = await pickFolder();
19
+ if (path) {
20
+ onSelect?.(path);
21
+ }
22
+ } finally {
23
+ setLoading(false);
24
+ }
25
+ }
26
+
27
+ return (
28
+ <button
29
+ type="button"
30
+ data-testid="add-repository-button"
31
+ onClick={handleClick}
32
+ disabled={loading}
33
+ className="border-muted-foreground/30 flex cursor-pointer items-center gap-2 rounded-full border-2 border-dashed px-3 py-1.5 text-xs transition-colors hover:border-blue-500 hover:text-blue-500 disabled:cursor-wait disabled:opacity-60"
34
+ >
35
+ {loading ? (
36
+ <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />
37
+ ) : (
38
+ <Plus className="h-3.5 w-3.5 shrink-0" />
39
+ )}
40
+ <span className="font-medium">{loading ? 'Opening...' : 'Add Repository'}</span>
41
+ </button>
42
+ );
43
+ }
@@ -1,2 +1,3 @@
1
1
  export { AddRepositoryNode } from './add-repository-node';
2
+ export { AddRepositoryButton } from './add-repository-button';
2
3
  export type { AddRepositoryNodeData, AddRepositoryNodeType } from './add-repository-node-config';
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useCallback, useEffect } from 'react';
3
+ import { useState, useCallback, useEffect, useRef } from 'react';
4
4
  import type { LucideIcon } from 'lucide-react';
5
5
  import {
6
6
  XIcon,
@@ -10,6 +10,8 @@ import {
10
10
  ImageIcon,
11
11
  CodeIcon,
12
12
  Trash2Icon,
13
+ ChevronsUpDown,
14
+ CheckIcon,
13
15
  } from 'lucide-react';
14
16
  import { cn } from '@/lib/utils';
15
17
  import {
@@ -28,12 +30,19 @@ import { CheckboxGroup } from '@/components/ui/checkbox-group';
28
30
  import { CheckboxGroupItem } from '@/components/ui/checkbox-group-item';
29
31
  import { Separator } from '@/components/ui/separator';
30
32
  import { Badge } from '@/components/ui/badge';
33
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
31
34
  import type { FileAttachment } from '@shepai/core/infrastructure/services/file-dialog.service';
32
35
  import type { WorkflowDefaults } from '@/app/actions/get-workflow-defaults';
33
36
  import { pickFiles } from './pick-files';
34
37
 
35
38
  export type { FileAttachment } from '@shepai/core/infrastructure/services/file-dialog.service';
36
39
 
40
+ /** Minimal feature descriptor for the parent selector. */
41
+ export interface ParentFeatureOption {
42
+ id: string;
43
+ name: string;
44
+ }
45
+
37
46
  export interface FeatureCreatePayload {
38
47
  name: string;
39
48
  description?: string;
@@ -46,6 +55,7 @@ export interface FeatureCreatePayload {
46
55
  };
47
56
  push: boolean;
48
57
  openPr: boolean;
58
+ parentId?: string;
49
59
  }
50
60
 
51
61
  const AUTO_APPROVE_OPTIONS = [
@@ -67,6 +77,10 @@ export interface FeatureCreateDrawerProps {
67
77
  repositoryPath: string;
68
78
  isSubmitting?: boolean;
69
79
  workflowDefaults?: WorkflowDefaults;
80
+ /** List of existing features available for selection as a parent. */
81
+ features?: ParentFeatureOption[];
82
+ /** Pre-select a parent feature when the drawer opens (e.g. from (+) button on a feature node). */
83
+ initialParentId?: string;
70
84
  }
71
85
 
72
86
  export function FeatureCreateDrawer({
@@ -76,6 +90,8 @@ export function FeatureCreateDrawer({
76
90
  repositoryPath,
77
91
  isSubmitting = false,
78
92
  workflowDefaults,
93
+ features,
94
+ initialParentId,
79
95
  }: FeatureCreateDrawerProps) {
80
96
  const defaultGates = workflowDefaults?.approvalGates ?? EMPTY_GATES;
81
97
  const defaultPush = workflowDefaults?.push ?? false;
@@ -87,6 +103,7 @@ export function FeatureCreateDrawer({
87
103
  const [approvalGates, setApprovalGates] = useState<Record<string, boolean>>({ ...defaultGates });
88
104
  const [push, setPush] = useState(defaultPush);
89
105
  const [openPr, setOpenPr] = useState(defaultOpenPr);
106
+ const [parentId, setParentId] = useState<string | undefined>(undefined);
90
107
 
91
108
  // Sync state when workflowDefaults load asynchronously
92
109
  useEffect(() => {
@@ -97,6 +114,13 @@ export function FeatureCreateDrawer({
97
114
  }
98
115
  }, [workflowDefaults]);
99
116
 
117
+ // Pre-select parent when initialParentId changes (e.g. (+) button on feature node)
118
+ useEffect(() => {
119
+ if (open && initialParentId) {
120
+ setParentId(initialParentId);
121
+ }
122
+ }, [open, initialParentId]);
123
+
100
124
  const resetForm = useCallback(() => {
101
125
  setName('');
102
126
  setDescription('');
@@ -104,6 +128,7 @@ export function FeatureCreateDrawer({
104
128
  setApprovalGates({ ...defaultGates });
105
129
  setPush(defaultPush);
106
130
  setOpenPr(defaultOpenPr);
131
+ setParentId(undefined);
107
132
  }, [defaultGates, defaultPush, defaultOpenPr]);
108
133
 
109
134
  const handleOpenChange = useCallback(
@@ -133,9 +158,20 @@ export function FeatureCreateDrawer({
133
158
  },
134
159
  push: push || openPr,
135
160
  openPr,
161
+ ...(parentId ? { parentId } : {}),
136
162
  });
137
163
  },
138
- [name, description, attachments, approvalGates, repositoryPath, onSubmit, push, openPr]
164
+ [
165
+ name,
166
+ description,
167
+ attachments,
168
+ approvalGates,
169
+ repositoryPath,
170
+ onSubmit,
171
+ push,
172
+ openPr,
173
+ parentId,
174
+ ]
139
175
  );
140
176
 
141
177
  const handleAddFiles = useCallback(async () => {
@@ -153,6 +189,8 @@ export function FeatureCreateDrawer({
153
189
  setAttachments((prev) => prev.filter((f) => f.path !== path));
154
190
  }, []);
155
191
 
192
+ const hasFeatures = features && features.length > 0;
193
+
156
194
  return (
157
195
  <Drawer direction="right" modal={false} handleOnly open={open} onOpenChange={handleOpenChange}>
158
196
  <DrawerContent direction="right" className="w-96" showCloseButton={false}>
@@ -220,6 +258,24 @@ export function FeatureCreateDrawer({
220
258
  />
221
259
  </div>
222
260
 
261
+ {/* Parent feature selector (only when features are available) */}
262
+ {hasFeatures ? (
263
+ <div className="flex flex-col gap-1.5">
264
+ <Label
265
+ htmlFor="parent-feature"
266
+ className="text-muted-foreground text-xs font-semibold tracking-wider"
267
+ >
268
+ PARENT FEATURE
269
+ </Label>
270
+ <ParentFeatureCombobox
271
+ features={features}
272
+ value={parentId}
273
+ onChange={setParentId}
274
+ disabled={isSubmitting}
275
+ />
276
+ </div>
277
+ ) : null}
278
+
223
279
  {/* Auto-approve checkboxes */}
224
280
  <div className="flex flex-col gap-1.5">
225
281
  <Label className="text-muted-foreground text-xs font-semibold tracking-wider">
@@ -313,6 +369,147 @@ export function FeatureCreateDrawer({
313
369
  );
314
370
  }
315
371
 
372
+ /* ---------------------------------------------------------------------------
373
+ * ParentFeatureCombobox — searchable dropdown for parent feature selection
374
+ * ------------------------------------------------------------------------- */
375
+
376
+ interface ParentFeatureComboboxProps {
377
+ features: ParentFeatureOption[];
378
+ value: string | undefined;
379
+ onChange: (id: string | undefined) => void;
380
+ disabled?: boolean;
381
+ }
382
+
383
+ function ParentFeatureCombobox({
384
+ features,
385
+ value,
386
+ onChange,
387
+ disabled,
388
+ }: ParentFeatureComboboxProps) {
389
+ const [open, setOpen] = useState(false);
390
+ const [query, setQuery] = useState('');
391
+ const inputRef = useRef<HTMLInputElement>(null);
392
+
393
+ const selectedFeature = features.find((f) => f.id === value);
394
+
395
+ const filtered = query.trim()
396
+ ? features.filter(
397
+ (f) =>
398
+ f.name.toLowerCase().includes(query.toLowerCase()) ||
399
+ f.id.toLowerCase().includes(query.toLowerCase())
400
+ )
401
+ : features;
402
+
403
+ const handleSelect = useCallback(
404
+ (id: string | undefined) => {
405
+ onChange(id);
406
+ setOpen(false);
407
+ setQuery('');
408
+ },
409
+ [onChange]
410
+ );
411
+
412
+ useEffect(() => {
413
+ if (open) {
414
+ setTimeout(() => inputRef.current?.focus(), 0);
415
+ } else {
416
+ setQuery('');
417
+ }
418
+ }, [open]);
419
+
420
+ return (
421
+ <Popover open={open} onOpenChange={setOpen}>
422
+ <PopoverTrigger asChild>
423
+ <button
424
+ id="parent-feature"
425
+ type="button"
426
+ role="combobox"
427
+ aria-expanded={open}
428
+ aria-label="Parent Feature"
429
+ disabled={disabled}
430
+ data-testid="parent-feature-combobox"
431
+ className={cn(
432
+ 'border-input bg-background ring-offset-background focus:ring-ring flex h-9 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
433
+ !selectedFeature && 'text-muted-foreground'
434
+ )}
435
+ >
436
+ <span className="truncate">
437
+ {selectedFeature
438
+ ? `${selectedFeature.name} (${selectedFeature.id.slice(0, 8)})`
439
+ : 'Select parent feature...'}
440
+ </span>
441
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
442
+ </button>
443
+ </PopoverTrigger>
444
+ <PopoverContent
445
+ className="w-80 p-0"
446
+ align="start"
447
+ data-testid="parent-feature-combobox-content"
448
+ >
449
+ <div className="flex flex-col">
450
+ {/* Search input */}
451
+ <div className="border-b p-2">
452
+ <Input
453
+ ref={inputRef}
454
+ placeholder="Search features..."
455
+ value={query}
456
+ onChange={(e) => setQuery(e.target.value)}
457
+ className="h-8 border-0 p-0 text-sm shadow-none focus-visible:ring-0"
458
+ data-testid="parent-feature-search"
459
+ />
460
+ </div>
461
+
462
+ {/* Options list */}
463
+ <div className="max-h-48 overflow-y-auto py-1" role="listbox" aria-label="Features">
464
+ {/* No parent option */}
465
+ <button
466
+ type="button"
467
+ role="option"
468
+ aria-selected={value === undefined}
469
+ onClick={() => handleSelect(undefined)}
470
+ className={cn(
471
+ 'hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 px-3 py-2 text-sm',
472
+ value === undefined && 'bg-accent/50'
473
+ )}
474
+ data-testid="parent-feature-option-none"
475
+ >
476
+ <CheckIcon className={cn('h-4 w-4 shrink-0', value !== undefined && 'invisible')} />
477
+ <span className="text-muted-foreground italic">No parent</span>
478
+ </button>
479
+
480
+ {filtered.length === 0 && query ? (
481
+ <p className="text-muted-foreground px-3 py-2 text-sm">No features found.</p>
482
+ ) : (
483
+ filtered.map((f) => (
484
+ <button
485
+ key={f.id}
486
+ type="button"
487
+ role="option"
488
+ aria-selected={value === f.id}
489
+ onClick={() => handleSelect(f.id)}
490
+ className={cn(
491
+ 'hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 px-3 py-2 text-sm',
492
+ value === f.id && 'bg-accent/50'
493
+ )}
494
+ data-testid={`parent-feature-option-${f.id}`}
495
+ >
496
+ <CheckIcon className={cn('h-4 w-4 shrink-0', value !== f.id && 'invisible')} />
497
+ <span className="truncate">
498
+ {f.name}{' '}
499
+ <span className="text-muted-foreground font-mono text-xs">
500
+ ({f.id.slice(0, 8)})
501
+ </span>
502
+ </span>
503
+ </button>
504
+ ))
505
+ )}
506
+ </div>
507
+ </div>
508
+ </PopoverContent>
509
+ </Popover>
510
+ );
511
+ }
512
+
316
513
  /* ---------------------------------------------------------------------------
317
514
  * Private sub-components & utilities
318
515
  * ------------------------------------------------------------------------- */
@@ -21,6 +21,11 @@ import type { FeatureNodeState } from './feature-node-state-config';
21
21
  * 6. No agent run → fall back to plan tasks / lifecycle
22
22
  */
23
23
  export function deriveNodeState(feature: Feature, agentRun?: AgentRun | null): FeatureNodeState {
24
+ // Blocked lifecycle takes priority — child waiting on parent regardless of agent run
25
+ if (feature.lifecycle === SdlcLifecycle.Blocked) {
26
+ return 'blocked';
27
+ }
28
+
24
29
  if (agentRun) {
25
30
  switch (agentRun.status) {
26
31
  case AgentRunStatus.waitingApproval:
@@ -100,7 +100,7 @@ export function FeatureNode({
100
100
  data-testid="feature-node-lifecycle-label"
101
101
  className={cn('text-[10px] font-semibold tracking-wider')}
102
102
  >
103
- {lifecycleDisplayLabels[data.lifecycle]}
103
+ {data.state === 'blocked' ? 'BLOCKED' : lifecycleDisplayLabels[data.lifecycle]}
104
104
  </span>
105
105
  {data.onSettings ? (
106
106
  <button
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import type { Edge } from '@xyflow/react';
5
5
  import { toast } from 'sonner';
6
6
  import type {
@@ -18,6 +18,7 @@ import type { TechDecisionsReviewData } from '@/components/common/tech-decisions
18
18
  import type { MergeReviewData } from '@/components/common/merge-review';
19
19
  import { FeaturesCanvas } from '@/components/features/features-canvas';
20
20
  import type { CanvasNodeType } from '@/components/features/features-canvas';
21
+ import type { FeatureNodeData } from '@/components/common/feature-node';
21
22
  import { FeatureDrawer, FeatureCreateDrawer } from '@/components/common';
22
23
  import { PrdQuestionnaireDrawer } from '@/components/common/prd-questionnaire';
23
24
  import type { PrdQuestionnaireData } from '@/components/common/prd-questionnaire';
@@ -52,8 +53,22 @@ export function ControlCenterInner({ initialNodes, initialEdges }: ControlCenter
52
53
  handleDeleteFeature,
53
54
  handleDeleteRepository,
54
55
  closeCreateDrawer,
56
+ pendingParentFeatureId,
55
57
  } = useControlCenterState(initialNodes, initialEdges);
56
58
 
59
+ // Extract feature list for the parent selector in the create drawer
60
+ const featureOptions = useMemo(
61
+ () =>
62
+ nodes
63
+ .filter((n) => n.type === 'featureNode')
64
+ .map((n) => {
65
+ const data = n.data as FeatureNodeData;
66
+ return { id: data.featureId, name: data.name };
67
+ })
68
+ .filter((f) => f.id && !f.id.startsWith('#')), // exclude optimistic temp nodes
69
+ [nodes]
70
+ );
71
+
57
72
  // Workflow defaults for the create-feature drawer
58
73
  const [workflowDefaults, setWorkflowDefaults] = useState<WorkflowDefaults | undefined>();
59
74
  useEffect(() => {
@@ -321,6 +336,8 @@ export function ControlCenterInner({ initialNodes, initialEdges }: ControlCenter
321
336
  onSubmit={handleCreateFeatureSubmit}
322
337
  repositoryPath={pendingRepositoryPath}
323
338
  workflowDefaults={workflowDefaults}
339
+ features={featureOptions}
340
+ initialParentId={pendingParentFeatureId}
324
341
  />
325
342
  </>
326
343
  );
@@ -414,6 +431,8 @@ export function ControlCenterInner({ initialNodes, initialEdges }: ControlCenter
414
431
  onSubmit={handleCreateFeatureSubmit}
415
432
  repositoryPath={pendingRepositoryPath}
416
433
  workflowDefaults={workflowDefaults}
434
+ features={featureOptions}
435
+ initialParentId={pendingParentFeatureId}
417
436
  />
418
437
  </>
419
438
  );
@@ -21,6 +21,8 @@ export interface ControlCenterState {
21
21
  selectedNode: FeatureNodeData | null;
22
22
  isCreateDrawerOpen: boolean;
23
23
  pendingRepositoryPath: string;
24
+ /** Parent feature ID pre-selected when (+) is clicked on a feature node. */
25
+ pendingParentFeatureId: string | undefined;
24
26
  onNodesChange: (changes: NodeChange<CanvasNodeType>[]) => void;
25
27
  handleConnect: (connection: Connection) => void;
26
28
  clearSelection: () => void;
@@ -191,7 +193,11 @@ export function useControlCenterState(
191
193
  );
192
194
 
193
195
  const createFeatureNode = useCallback(
194
- (sourceNodeId: string | null, dataOverride?: Partial<FeatureNodeData>): string => {
196
+ (
197
+ sourceNodeId: string | null,
198
+ dataOverride?: Partial<FeatureNodeData>,
199
+ edgeType?: string
200
+ ): string => {
195
201
  const id = `feature-${Date.now()}-${nextFeatureId++}`;
196
202
  const newFeatureData: FeatureNodeData = {
197
203
  name: dataOverride?.name ?? 'New Feature',
@@ -292,10 +298,13 @@ export function useControlCenterState(
292
298
  setEdges((currentEdges) => [
293
299
  ...currentEdges,
294
300
  {
295
- id: `edge-${sourceNodeId}-${id}`,
301
+ id:
302
+ edgeType === 'dependencyEdge'
303
+ ? `dep-${sourceNodeId}-${id}`
304
+ : `edge-${sourceNodeId}-${id}`,
296
305
  source: sourceNodeId,
297
306
  target: id,
298
- style: { strokeDasharray: '5 5' },
307
+ ...(edgeType ? { type: edgeType } : { style: { strokeDasharray: '5 5' } }),
299
308
  },
300
309
  ]);
301
310
  }
@@ -314,19 +323,30 @@ export function useControlCenterState(
314
323
  setIsCreateDrawerOpen(true);
315
324
  }, []);
316
325
 
326
+ const [pendingParentFeatureId, setPendingParentFeatureId] = useState<string | undefined>();
327
+
317
328
  const handleCreateFeatureSubmit = useCallback(
318
329
  (data: FeatureCreatePayload) => {
319
330
  // 1. Insert optimistic node instantly
320
- const tempId = createFeatureNode(pendingRepoNodeId, {
321
- state: 'creating',
322
- name: data.name,
323
- description: data.description,
324
- repositoryPath: data.repositoryPath,
325
- });
331
+ // For child features, connect to the parent feature node instead of the repo node
332
+ const sourceNodeId = pendingParentFeatureId
333
+ ? `feat-${pendingParentFeatureId}`
334
+ : pendingRepoNodeId;
335
+ const tempId = createFeatureNode(
336
+ sourceNodeId,
337
+ {
338
+ state: 'creating',
339
+ name: data.name,
340
+ description: data.description,
341
+ repositoryPath: data.repositoryPath,
342
+ },
343
+ pendingParentFeatureId ? 'dependencyEdge' : undefined
344
+ );
326
345
 
327
346
  // 2. Close drawer and clear pending state immediately
328
347
  setIsCreateDrawerOpen(false);
329
348
  setPendingRepoNodeId(null);
349
+ setPendingParentFeatureId(undefined);
330
350
 
331
351
  // 3. Fire server action in the background
332
352
  createFeature(data)
@@ -348,11 +368,12 @@ export function useControlCenterState(
348
368
  toast.error('Failed to create feature');
349
369
  });
350
370
  },
351
- [router, createFeatureNode, pendingRepoNodeId, setEdges]
371
+ [router, createFeatureNode, pendingRepoNodeId, pendingParentFeatureId, setEdges]
352
372
  );
353
373
 
354
374
  const closeCreateDrawer = useCallback(() => {
355
375
  setIsCreateDrawerOpen(false);
376
+ setPendingParentFeatureId(undefined);
356
377
  }, []);
357
378
 
358
379
  const handleDeleteFeature = useCallback(
@@ -426,9 +447,18 @@ export function useControlCenterState(
426
447
 
427
448
  const handleAddFeatureToFeature = useCallback(
428
449
  (featureNodeId: string) => {
429
- createFeatureNode(featureNodeId);
450
+ // Extract feature ID from node ID (format: "feat-<uuid>")
451
+ const featureId = featureNodeId.startsWith('feat-') ? featureNodeId.slice(5) : featureNodeId;
452
+ // Find the repo node that owns this feature
453
+ const repoEdge = edges.find((e) => e.target === featureNodeId);
454
+ const repoNodeId = repoEdge?.source ?? null;
455
+
456
+ setSelectedNode(null);
457
+ setPendingRepoNodeId(repoNodeId);
458
+ setPendingParentFeatureId(featureId);
459
+ setIsCreateDrawerOpen(true);
430
460
  },
431
- [createFeatureNode]
461
+ [edges]
432
462
  );
433
463
 
434
464
  const handleLayout = useCallback(
@@ -564,6 +594,7 @@ export function useControlCenterState(
564
594
  selectedNode,
565
595
  isCreateDrawerOpen,
566
596
  pendingRepositoryPath,
597
+ pendingParentFeatureId,
567
598
  onNodesChange,
568
599
  handleConnect,
569
600
  clearSelection,
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ReactFlow, ReactFlowProvider } from '@xyflow/react';
3
+ import { DependencyEdge } from './dependency-edge';
4
+ import { FeatureNode } from '@/components/common/feature-node';
5
+ import type { FeatureNodeData } from '@/components/common/feature-node';
6
+
7
+ import '@xyflow/react/dist/style.css';
8
+
9
+ const meta: Meta = {
10
+ title: 'Features/DependencyEdge',
11
+ decorators: [
12
+ (Story) => (
13
+ <div style={{ width: '800px', height: '400px' }}>
14
+ <ReactFlowProvider>
15
+ <Story />
16
+ </ReactFlowProvider>
17
+ </div>
18
+ ),
19
+ ],
20
+ };
21
+
22
+ export default meta;
23
+
24
+ const parentData: FeatureNodeData = {
25
+ name: 'Auth System',
26
+ description: 'User authentication',
27
+ featureId: 'feat-1234',
28
+ lifecycle: 'implementation',
29
+ state: 'running',
30
+ progress: 60,
31
+ repositoryPath: '/repo',
32
+ branch: 'feat/auth',
33
+ showHandles: true,
34
+ };
35
+
36
+ const childData: FeatureNodeData = {
37
+ name: 'Login Page',
38
+ description: 'Login UI component',
39
+ featureId: 'feat-5678',
40
+ lifecycle: 'requirements',
41
+ state: 'blocked',
42
+ progress: 0,
43
+ repositoryPath: '/repo',
44
+ branch: 'feat/login',
45
+ blockedBy: 'Auth System',
46
+ showHandles: true,
47
+ };
48
+
49
+ export const Default: StoryObj = {
50
+ render: () => (
51
+ <ReactFlow
52
+ nodes={[
53
+ {
54
+ id: 'parent',
55
+ type: 'featureNode',
56
+ position: { x: 50, y: 100 },
57
+ data: parentData,
58
+ },
59
+ {
60
+ id: 'child',
61
+ type: 'featureNode',
62
+ position: { x: 450, y: 100 },
63
+ data: childData,
64
+ },
65
+ ]}
66
+ edges={[
67
+ {
68
+ id: 'dep-1',
69
+ source: 'parent',
70
+ target: 'child',
71
+ type: 'dependencyEdge',
72
+ },
73
+ ]}
74
+ nodeTypes={{ featureNode: FeatureNode }}
75
+ edgeTypes={{ dependencyEdge: DependencyEdge }}
76
+ fitView
77
+ nodesDraggable={false}
78
+ nodesConnectable={false}
79
+ />
80
+ ),
81
+ };
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { BaseEdge, getBezierPath } from '@xyflow/react';
4
+ import type { EdgeProps } from '@xyflow/react';
5
+
6
+ /**
7
+ * Custom React Flow edge for parent→child feature dependencies.
8
+ * Uses bezier curves and dashed style matching repo→feature edges.
9
+ */
10
+ export function DependencyEdge(props: EdgeProps) {
11
+ const [edgePath] = getBezierPath({
12
+ sourceX: props.sourceX,
13
+ sourceY: props.sourceY,
14
+ targetX: props.targetX,
15
+ targetY: props.targetY,
16
+ sourcePosition: props.sourcePosition,
17
+ targetPosition: props.targetPosition,
18
+ });
19
+
20
+ return (
21
+ <BaseEdge
22
+ id={props.id}
23
+ path={edgePath}
24
+ style={{
25
+ strokeDasharray: '5 5',
26
+ }}
27
+ />
28
+ );
29
+ }