@assistkick/create 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/bin/create.js +0 -0
  2. package/package.json +9 -7
  3. package/templates/assistkick-product-system/.env.example +1 -0
  4. package/templates/assistkick-product-system/local.db +0 -0
  5. package/templates/assistkick-product-system/package.json +4 -2
  6. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  10. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  11. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
  13. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  14. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  15. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  16. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  18. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  24. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  28. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  30. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  33. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
  42. package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
  62. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
  63. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  64. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  65. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  66. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  67. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  68. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  69. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  109. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  113. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  114. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  123. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  124. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  133. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  134. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
  157. package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  159. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  160. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  161. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
  163. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  165. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  166. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  167. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  168. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  171. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  172. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  173. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  174. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  175. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  176. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  177. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  178. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  179. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  180. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  181. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  182. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  183. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  184. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  185. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  186. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  187. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  188. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  189. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  190. package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
  191. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  192. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  193. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  194. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  195. package/templates/skills/product-system/graph.json +1890 -0
  196. package/templates/skills/product-system/kanban.json +304 -0
  197. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  198. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  199. package/templates/skills/product-system/nodes/data_001.md +51 -0
  200. package/templates/skills/product-system/nodes/data_002.md +40 -0
  201. package/templates/skills/product-system/nodes/data_004.md +38 -0
  202. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  203. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  204. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  205. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  206. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -1,8 +1,10 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from 'react';
2
2
  import { apiClient } from '../api/client';
3
- import { COLUMNS, PIPELINE_STATUS_LABELS } from '../constants/graph';
3
+ import { COLUMNS, WORKFLOW_STATUS_LABELS } from '../constants/graph';
4
4
  import { useToast } from '../hooks/useToast';
5
5
  import { KanbanCard } from './ds/KanbanCard';
6
+ import { IterationCommentModal } from './IterationCommentModal';
7
+ import { WorkflowMonitorModal } from './workflow/WorkflowMonitorModal';
6
8
 
7
9
 
8
10
  interface KanbanViewProps {
@@ -17,6 +19,10 @@ interface CardData {
17
19
  name: string;
18
20
  completeness: number;
19
21
  kind: string;
22
+ nodeType: string;
23
+ featureType: string | null;
24
+ isEpicChild: boolean;
25
+ epicParentId: string | null;
20
26
  rejectionCount: number;
21
27
  notes: any[];
22
28
  reviews: any[];
@@ -25,62 +31,48 @@ interface CardData {
25
31
  movedAt: string | null;
26
32
  }
27
33
 
28
- const ALL_COLUMNS = COLUMNS.map(c => c.id);
29
-
30
- const STAGE_TABS = [
31
- { key: 'in_progress', label: 'Dev' },
32
- { key: 'in_review', label: 'Review' },
33
- { key: 'qa', label: 'QA' },
34
- ] as const;
35
-
36
- const hasStageData = (stage: any): boolean => {
37
- if (!stage) return false;
38
- const tc = stage.toolCalls;
39
- return (tc && tc.total > 0)
40
- || stage.numTurns != null
41
- || stage.costUsd != null
42
- || stage.usage != null
43
- || stage.stopReason != null;
44
- };
45
-
46
- const formatContextFill = (lastTurnUsage: any, contextWindow: number | null): string => {
47
- if (!lastTurnUsage) return '';
48
- const fill = (lastTurnUsage.input_tokens || 0)
49
- + (lastTurnUsage.cache_creation_input_tokens || 0)
50
- + (lastTurnUsage.cache_read_input_tokens || 0);
51
- const denom = contextWindow || 200_000;
52
- const pct = Math.round((fill / denom) * 100);
53
- return `${pct}%`;
54
- };
55
-
56
- const formatCost = (costUsd: number | null): string => {
57
- if (costUsd == null) return '';
58
- return `$${costUsd.toFixed(4)}`;
59
- };
60
-
61
- const formatModel = (model: string | null): string => {
62
- if (!model) return '';
63
- // Shorten long model names: "claude-sonnet-4-20250514" → "sonnet-4"
64
- const match = model.match(/(opus|sonnet|haiku)-(\d[\d.]*)/);
65
- return match ? `${match[1]}-${match[2]}` : model;
66
- };
34
+ interface WorkflowStatus {
35
+ executionId?: string;
36
+ status: string;
37
+ featureId: string;
38
+ currentNodeId?: string | null;
39
+ currentNodeType?: string | null;
40
+ currentAgentName?: string | null;
41
+ error?: string | null;
42
+ completedNodes?: string[];
43
+ failedNodes?: string[];
44
+ pendingNodes?: string[];
45
+ }
46
+
47
+ interface WorkflowListItem {
48
+ id: string;
49
+ name: string;
50
+ featureType: string | null;
51
+ triggerColumn: string | null;
52
+ isDefault: number;
53
+ }
67
54
 
68
55
  export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }: KanbanViewProps) {
69
56
  const [kanbanData, setKanbanData] = useState<any>(null);
70
57
  const [error, setError] = useState<string | null>(null);
71
58
  const [draggedCard, setDraggedCard] = useState<string | null>(null);
72
59
  const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
73
- const [pipelineStatuses, setPipelineStatuses] = useState<Record<string, any>>({});
60
+ const [workflowStatuses, setWorkflowStatuses] = useState<Record<string, WorkflowStatus>>({});
74
61
  const [playAllRunning, setPlayAllRunning] = useState(false);
75
62
  const [playAllCurrentFeature, setPlayAllCurrentFeature] = useState<string | null>(null);
63
+ const [playAllEpicId, setPlayAllEpicId] = useState<string | null>(null);
76
64
  const [copiedId, setCopiedId] = useState<string | null>(null);
77
- const [activeStageTab, setActiveStageTab] = useState<Record<string, string>>({});
65
+ const [availableWorkflows, setAvailableWorkflows] = useState<WorkflowListItem[]>([]);
66
+ const [dropdownOpen, setDropdownOpen] = useState<string | null>(null);
67
+ const [monitorTarget, setMonitorTarget] = useState<{ featureId: string; featureName: string } | null>(null);
68
+ const [backwardMove, setBackwardMove] = useState<{ featureId: string; featureName: string; fromColumn: string; toColumn: string } | null>(null);
78
69
  const { showToast } = useToast();
79
70
 
80
71
  const pollersRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
81
72
  const orchestratorPollerRef = useRef<ReturnType<typeof setInterval> | null>(null);
82
73
  const kanbanDataRef = useRef(kanbanData);
83
74
  kanbanDataRef.current = kanbanData;
75
+ const backwardMoveTargetRef = useRef<string | null>(null);
84
76
 
85
77
  const fetchKanban = useCallback(async () => {
86
78
  try {
@@ -94,10 +86,16 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
94
86
  }
95
87
  }, [projectId]);
96
88
 
89
+ // Load available workflows for the dropdown
90
+ useEffect(() => {
91
+ apiClient.fetchWorkflows(projectId ?? undefined).then(data => {
92
+ setAvailableWorkflows(data.workflows || []);
93
+ }).catch(() => { /* ignore */ });
94
+ }, [projectId]);
95
+
97
96
  useEffect(() => {
98
97
  fetchKanban();
99
98
  return () => {
100
- // Cleanup all pollers
101
99
  for (const intervalId of pollersRef.current.values()) {
102
100
  clearInterval(intervalId);
103
101
  }
@@ -111,18 +109,33 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
111
109
 
112
110
  const getCardsForColumn = useCallback((columnId: string): CardData[] => {
113
111
  if (!kanbanData || !graphData) return [];
114
- const featureNodes = new Map<string, any>();
112
+ const kanbanNodes = new Map<string, any>();
115
113
  graphData.nodes
116
- .filter((n: any) => n.type === 'feature')
117
- .forEach((n: any) => featureNodes.set(n.id, n));
114
+ .filter((n: any) => n.type === 'feature' || n.type === 'epic')
115
+ .forEach((n: any) => kanbanNodes.set(n.id, n));
116
+
117
+ // Build epic → children map from backend-provided epic_parent_id
118
+ const epicChildrenMap = new Map<string, Set<string>>();
119
+ for (const [id, entry] of Object.entries(kanbanData) as [string, any][]) {
120
+ if (entry.epic_parent_id) {
121
+ if (!epicChildrenMap.has(entry.epic_parent_id)) {
122
+ epicChildrenMap.set(entry.epic_parent_id, new Set());
123
+ }
124
+ epicChildrenMap.get(entry.epic_parent_id)!.add(id);
125
+ }
126
+ }
118
127
 
119
128
  const cards: CardData[] = Object.entries(kanbanData)
120
- .filter(([id, entry]: [string, any]) => entry.column === columnId && featureNodes.has(id))
129
+ .filter(([id, entry]: [string, any]) => entry.column === columnId && kanbanNodes.has(id))
121
130
  .map(([id, entry]: [string, any]) => ({
122
131
  id,
123
- name: featureNodes.get(id).name,
124
- completeness: featureNodes.get(id).completeness,
125
- kind: featureNodes.get(id).kind || 'new',
132
+ name: kanbanNodes.get(id).name,
133
+ completeness: kanbanNodes.get(id).completeness,
134
+ kind: kanbanNodes.get(id).kind || 'new',
135
+ nodeType: kanbanNodes.get(id).type || 'feature',
136
+ featureType: kanbanNodes.get(id).feature_type || null,
137
+ isEpicChild: false,
138
+ epicParentId: entry.epic_parent_id || null,
126
139
  rejectionCount: entry.rejection_count || 0,
127
140
  notes: entry.notes || [],
128
141
  reviews: entry.reviews || [],
@@ -139,22 +152,45 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
139
152
  cards.sort((a, b) => (a.movedAt || '').localeCompare(b.movedAt || ''));
140
153
  }
141
154
 
142
- return cards;
155
+ // Sort-order-independent grouping: epics first, then their children, then ungrouped
156
+ const epicCards = cards.filter(c => c.nodeType === 'epic');
157
+ const childIds = new Set<string>();
158
+ const grouped: CardData[] = [];
159
+
160
+ for (const epic of epicCards) {
161
+ grouped.push(epic);
162
+ const children = epicChildrenMap.get(epic.id);
163
+ if (children) {
164
+ const childCards = cards.filter(c => children.has(c.id));
165
+ for (const child of childCards) {
166
+ child.isEpicChild = true;
167
+ grouped.push(child);
168
+ childIds.add(child.id);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Add ungrouped cards (not an epic and not an epic child)
174
+ for (const card of cards) {
175
+ if (card.nodeType !== 'epic' && !childIds.has(card.id)) {
176
+ grouped.push(card);
177
+ }
178
+ }
179
+
180
+ return grouped;
143
181
  }, [kanbanData, graphData]);
144
182
 
145
- const getPipelineBadgeText = (status: string, rejectionCount: number) => {
146
- const label = (PIPELINE_STATUS_LABELS as any)[status] || status;
147
- if (rejectionCount >= 1) return `Retry ${rejectionCount + 1}`;
148
- return label;
183
+ const getStatusBadgeText = (status: string) => {
184
+ return (WORKFLOW_STATUS_LABELS as any)[status] || status;
149
185
  };
150
186
 
151
- const startPipelinePolling = useCallback((featureId: string) => {
187
+ const startWorkflowPolling = useCallback((featureId: string) => {
152
188
  if (pollersRef.current.has(featureId)) return;
153
189
  const intervalId = setInterval(async () => {
154
190
  try {
155
- const status = await apiClient.getPipelineStatus(featureId);
156
- setPipelineStatuses(prev => ({ ...prev, [featureId]: status }));
157
- if (['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(status.status)) {
191
+ const status: WorkflowStatus = await apiClient.getWorkflowStatus(featureId);
192
+ setWorkflowStatuses(prev => ({ ...prev, [featureId]: status }));
193
+ if (['idle', 'completed', 'failed'].includes(status.status)) {
158
194
  clearInterval(pollersRef.current.get(featureId)!);
159
195
  pollersRef.current.delete(featureId);
160
196
  await fetchKanban();
@@ -167,35 +203,22 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
167
203
  pollersRef.current.set(featureId, intervalId);
168
204
  }, [fetchKanban]);
169
205
 
170
- // Fetch initial pipeline statuses for all cards
206
+ // Fetch initial workflow statuses for all cards
171
207
  useEffect(() => {
172
208
  if (!kanbanData || !graphData) return;
173
209
  const featureIds = Object.keys(kanbanData);
174
210
  featureIds.forEach(async (id) => {
175
211
  try {
176
- const status = await apiClient.getPipelineStatus(id);
212
+ const status: WorkflowStatus = await apiClient.getWorkflowStatus(id);
177
213
  if (status.status !== 'idle') {
178
- setPipelineStatuses(prev => ({ ...prev, [id]: status }));
179
- if (!['completed', 'failed', 'blocked', 'interrupted'].includes(status.status)) {
180
- startPipelinePolling(id);
214
+ setWorkflowStatuses(prev => ({ ...prev, [id]: status }));
215
+ if (!['completed', 'failed'].includes(status.status)) {
216
+ startWorkflowPolling(id);
181
217
  }
182
218
  }
183
219
  } catch { /* ignore */ }
184
220
  });
185
- }, [kanbanData, graphData, startPipelinePolling]);
186
-
187
- const checkDependencies = useCallback((featureId: string) => {
188
- if (!graphData || !kanbanData) return { blocked: false, blockedBy: [] as string[] };
189
- const deps = graphData.edges
190
- .filter((e: any) => e.from === featureId && e.relation === 'depends_on')
191
- .map((e: any) => e.to)
192
- .filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
193
- const blockedBy = deps.filter((depId: string) => {
194
- const entry = kanbanData[depId];
195
- return !entry || entry.column !== 'done';
196
- });
197
- return { blocked: blockedBy.length > 0, blockedBy };
198
- }, [graphData, kanbanData]);
221
+ }, [kanbanData, graphData, startWorkflowPolling]);
199
222
 
200
223
  // Drag handlers
201
224
  const handleDragStart = (e: React.DragEvent, featureId: string) => {
@@ -229,6 +252,26 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
229
252
  setDragOverColumn(null);
230
253
  const featureId = e.dataTransfer.getData('text/plain');
231
254
  if (!featureId || !targetColumn) return;
255
+
256
+ // Detect backward move — prompt for iteration comment
257
+ const currentEntry = kanbanData?.[featureId];
258
+ if (currentEntry) {
259
+ const colIds = COLUMNS.map(c => c.id);
260
+ const currentIdx = colIds.indexOf(currentEntry.column);
261
+ const targetIdx = colIds.indexOf(targetColumn);
262
+ if (targetIdx < currentIdx) {
263
+ // Find the card name from graphData
264
+ const node = graphData?.nodes?.find((n: any) => n.id === featureId);
265
+ const featureName = node?.name || featureId;
266
+ const fromLabel = COLUMNS.find(c => c.id === currentEntry.column)?.label || currentEntry.column;
267
+ const toLabel = COLUMNS.find(c => c.id === targetColumn)?.label || targetColumn;
268
+ setBackwardMove({ featureId, featureName, fromColumn: fromLabel, toColumn: toLabel });
269
+ // Store the target for the confirm handler
270
+ backwardMoveTargetRef.current = targetColumn;
271
+ return;
272
+ }
273
+ }
274
+
232
275
  try {
233
276
  await apiClient.moveCard(featureId, targetColumn);
234
277
  await fetchKanban();
@@ -237,34 +280,44 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
237
280
  }
238
281
  };
239
282
 
240
- const handleStartPipeline = async (featureId: string) => {
283
+ const handleBackwardMoveConfirm = async (comment: string) => {
284
+ if (!backwardMove) return;
285
+ const targetColumn = backwardMoveTargetRef.current;
286
+ backwardMoveTargetRef.current = null;
287
+ setBackwardMove(null);
288
+ if (!targetColumn) return;
241
289
  try {
242
- await apiClient.startPipeline(featureId);
243
- startPipelinePolling(featureId);
290
+ await apiClient.moveCard(backwardMove.featureId, targetColumn, comment || undefined);
244
291
  await fetchKanban();
245
- } catch (err: any) {
246
- console.error('Failed to start pipeline:', err);
247
- showToast(err?.message || 'Failed to start pipeline');
292
+ } catch (err) {
293
+ console.error('Failed to move card:', err);
248
294
  }
249
295
  };
250
296
 
251
- const handleResumePipeline = async (featureId: string) => {
297
+ const handleBackwardMoveCancel = () => {
298
+ backwardMoveTargetRef.current = null;
299
+ setBackwardMove(null);
300
+ };
301
+
302
+ const handleStartWorkflow = async (featureId: string, workflowId?: string) => {
252
303
  try {
253
- await apiClient.resumePipeline(featureId);
254
- startPipelinePolling(featureId);
304
+ await apiClient.startWorkflow(featureId, workflowId, projectId ?? undefined);
305
+ startWorkflowPolling(featureId);
255
306
  await fetchKanban();
256
307
  } catch (err: any) {
257
- console.error('Failed to resume pipeline:', err);
258
- showToast(err?.message || 'Failed to resume pipeline');
308
+ console.error('Failed to start workflow:', err);
309
+ showToast(err?.message || 'Failed to start workflow');
259
310
  }
260
311
  };
261
312
 
262
- const handleUnblock = async (featureId: string) => {
313
+ const handleResumeWorkflow = async (featureId: string) => {
263
314
  try {
264
- await apiClient.unblockCard(featureId);
315
+ await apiClient.resumeWorkflow(featureId);
316
+ startWorkflowPolling(featureId);
265
317
  await fetchKanban();
266
- } catch (err) {
267
- console.error('Failed to unblock card:', err);
318
+ } catch (err: any) {
319
+ console.error('Failed to resume workflow:', err);
320
+ showToast(err?.message || 'Failed to resume workflow');
268
321
  }
269
322
  };
270
323
 
@@ -277,6 +330,18 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
277
330
  } catch { /* ignore */ }
278
331
  };
279
332
 
333
+ const handlePlayDropdown = (featureId: string) => {
334
+ setDropdownOpen(prev => prev === featureId ? null : featureId);
335
+ };
336
+
337
+ // Close dropdown when clicking outside
338
+ useEffect(() => {
339
+ if (!dropdownOpen) return;
340
+ const handler = () => setDropdownOpen(null);
341
+ document.addEventListener('click', handler);
342
+ return () => document.removeEventListener('click', handler);
343
+ }, [dropdownOpen]);
344
+
280
345
  // Orchestrator status polling
281
346
  const startOrchestratorPolling = useCallback(() => {
282
347
  if (orchestratorPollerRef.current) return;
@@ -285,14 +350,15 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
285
350
  const status = await apiClient.getOrchestratorStatus();
286
351
  setPlayAllRunning(status.active);
287
352
  setPlayAllCurrentFeature(status.currentFeatureId);
353
+ setPlayAllEpicId(status.epicId || null);
288
354
  if (status.active) {
289
355
  await fetchKanban();
290
356
  } else {
291
- // Orchestrator stopped — clean up poller
292
357
  if (orchestratorPollerRef.current) {
293
358
  clearInterval(orchestratorPollerRef.current);
294
359
  orchestratorPollerRef.current = null;
295
360
  }
361
+ setPlayAllEpicId(null);
296
362
  await fetchKanban();
297
363
  }
298
364
  } catch {
@@ -302,6 +368,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
302
368
  }
303
369
  setPlayAllRunning(false);
304
370
  setPlayAllCurrentFeature(null);
371
+ setPlayAllEpicId(null);
305
372
  }
306
373
  }, 5000);
307
374
  }, [fetchKanban]);
@@ -311,6 +378,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
311
378
  apiClient.getOrchestratorStatus().then(status => {
312
379
  setPlayAllRunning(status.active);
313
380
  setPlayAllCurrentFeature(status.currentFeatureId);
381
+ setPlayAllEpicId(status.epicId || null);
314
382
  if (status.active) {
315
383
  startOrchestratorPolling();
316
384
  }
@@ -325,10 +393,10 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
325
393
  }, [startOrchestratorPolling]);
326
394
 
327
395
  // Play All — thin wrapper around backend orchestrator
328
- const startPlayAll = async () => {
396
+ const startPlayAll = async (epicId?: string) => {
329
397
  if (playAllRunning) return;
330
398
  try {
331
- await apiClient.startPlayAll(projectId ?? undefined);
399
+ await apiClient.startPlayAll(projectId ?? undefined, epicId);
332
400
  setPlayAllRunning(true);
333
401
  setPlayAllCurrentFeature(null);
334
402
  startOrchestratorPolling();
@@ -352,7 +420,6 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
352
420
  <div className="flex h-full w-full gap-2 overflow-x-auto">
353
421
  {COLUMNS.map(col => {
354
422
  const cards = getCardsForColumn(col.id);
355
- const sourceColumn = draggedCard ? kanbanData?.[draggedCard]?.column : null;
356
423
  const isDragOver = dragOverColumn === col.id;
357
424
 
358
425
  return (
@@ -376,7 +443,7 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
376
443
  <button
377
444
  className="flex h-6 w-6 items-center justify-center rounded-full border border-accent text-accent text-[9px] tracking-[-2px] pl-0.5 hover:bg-accent hover:text-surface transition-colors cursor-pointer"
378
445
  title="Start automated development for all TODO features"
379
- onClick={startPlayAll}
446
+ onClick={() => startPlayAll()}
380
447
  >
381
448
  {'\u25B6\u25B6'}
382
449
  </button>
@@ -396,48 +463,12 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
396
463
  >
397
464
  {cards.map(card => {
398
465
  const pct = Math.round((card.completeness || 0) * 100);
399
- const pStatus = pipelineStatuses[card.id];
400
- const isActive = pStatus && !['idle', 'completed', 'blocked', 'failed', 'interrupted'].includes(pStatus.status);
401
- const pipelineStatusValue = pStatus?.status ?? 'idle';
466
+ const wStatus = workflowStatuses[card.id];
467
+ const workflowStatusValue = wStatus?.status ?? 'idle';
468
+ const isActive = workflowStatusValue === 'running';
402
469
  const showResumeBtn = ['in_progress', 'in_review'].includes(card.column)
403
470
  && !isActive
404
- && ['interrupted', 'failed', 'idle'].includes(pipelineStatusValue);
405
-
406
- // Build tool calls + meta from stage stats or flat toolCalls
407
- let toolCalls: Record<string, number> | undefined;
408
- let metaPills: string[] | undefined;
409
- let stopReason: string | undefined;
410
-
411
- const ss = pStatus?.stageStats;
412
- const availableTabs = ss
413
- ? STAGE_TABS.filter(t => hasStageData(ss[t.key]))
414
- : [];
415
-
416
- if (availableTabs.length > 0) {
417
- const currentTab = activeStageTab[card.id] || availableTabs[availableTabs.length - 1].key;
418
- const stage = ss[currentTab];
419
- const tc = stage?.toolCalls;
420
- if (tc && tc.total > 0) {
421
- toolCalls = {};
422
- if (tc.read > 0) toolCalls['Read'] = tc.read;
423
- if (tc.bash > 0) toolCalls['Tools'] = tc.bash;
424
- if (tc.write > 0) toolCalls['Write'] = tc.write;
425
- if (tc.edit > 0) toolCalls['Edit'] = tc.edit;
426
- }
427
- const pills: string[] = [];
428
- if (stage?.lastTurnUsage) pills.push(`Ctx ${formatContextFill(stage.lastTurnUsage, stage.contextWindow)}`);
429
- if (stage?.numTurns != null) pills.push(`${stage.numTurns}t`);
430
- if (stage?.costUsd != null) pills.push(formatCost(stage.costUsd));
431
- if (stage?.model) pills.push(formatModel(stage.model));
432
- if (pills.length > 0) metaPills = pills;
433
- if (stage?.stopReason) stopReason = stage.stopReason;
434
- } else if (pStatus?.toolCalls?.total > 0) {
435
- toolCalls = {};
436
- if (pStatus.toolCalls.read > 0) toolCalls['Read'] = pStatus.toolCalls.read;
437
- if (pStatus.toolCalls.bash > 0) toolCalls['Tools'] = pStatus.toolCalls.bash;
438
- if (pStatus.toolCalls.write > 0) toolCalls['Write'] = pStatus.toolCalls.write;
439
- if (pStatus.toolCalls.edit > 0) toolCalls['Edit'] = pStatus.toolCalls.edit;
440
- }
471
+ && workflowStatusValue === 'failed';
441
472
 
442
473
  const issueLabel = card.notes.length > 0
443
474
  ? `${card.notes.length} issue${card.notes.length !== 1 ? 's' : ''} reported`
@@ -446,35 +477,81 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
446
477
  : 'No issues';
447
478
 
448
479
  return (
449
- <KanbanCard
450
- key={card.id}
451
- id={card.id}
452
- name={card.name}
453
- pct={pct}
454
- kind={card.kind}
455
- rejectionCount={card.rejectionCount}
456
- blocked={card.devBlocked}
457
- pipeline={pipelineStatusValue}
458
- pipelineLabel={pStatus && pStatus.status !== 'idle' ? getPipelineBadgeText(pStatus.status, card.rejectionCount) : undefined}
459
- toolCalls={toolCalls}
460
- meta={metaPills}
461
- stopReason={stopReason}
462
- issueCount={card.notes.length}
463
- issueLabel={issueLabel}
464
- showPlay={card.column === 'todo' && !card.devBlocked}
465
- showResume={showResumeBtn}
466
- copied={copiedId === card.id}
467
- playAllActive={playAllCurrentFeature === card.id}
468
- onClick={() => onCardClick({ id: card.id, name: card.name })}
469
- onCopy={() => handleCopy(card)}
470
- onPlay={() => handleStartPipeline(card.id)}
471
- onResume={() => handleResumePipeline(card.id)}
472
- onUnblock={() => handleUnblock(card.id)}
473
- onIssuesClick={() => onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews)}
474
- draggable
475
- onDragStart={(e) => handleDragStart(e, card.id)}
476
- onDragEnd={handleDragEnd}
477
- />
480
+ <div key={card.id} className={['relative', card.isEpicChild ? 'ml-3' : ''].join(' ')}>
481
+ <KanbanCard
482
+ id={card.id}
483
+ name={card.name}
484
+ pct={pct}
485
+ kind={card.kind}
486
+ isEpic={card.nodeType === 'epic'}
487
+ rejectionCount={card.rejectionCount}
488
+ blocked={card.devBlocked}
489
+ pipeline={workflowStatusValue}
490
+ pipelineLabel={wStatus && wStatus.status !== 'idle' ? getStatusBadgeText(wStatus.status) : undefined}
491
+ issueCount={card.notes.length}
492
+ issueLabel={issueLabel}
493
+ showPlay={(() => {
494
+ if (card.devBlocked) return false;
495
+ if (card.nodeType === 'epic' && playAllRunning && playAllEpicId === card.id) return false;
496
+ // Default column for non-video features
497
+ if (!card.featureType || card.featureType === 'code') return card.column === 'todo';
498
+ // For video features, show play when a workflow matches the column
499
+ const cardType = card.featureType;
500
+ return availableWorkflows.some(wf =>
501
+ (!wf.featureType || wf.featureType === cardType)
502
+ && wf.triggerColumn === card.column
503
+ );
504
+ })()}
505
+ showPlayDropdown={card.nodeType !== 'epic' && availableWorkflows.length > 1}
506
+ showStop={card.nodeType === 'epic' && playAllRunning && playAllEpicId === card.id}
507
+ showResume={showResumeBtn}
508
+ copied={copiedId === card.id}
509
+ playAllActive={playAllCurrentFeature === card.id || (card.nodeType === 'epic' && playAllRunning && playAllEpicId === card.id)}
510
+ agentName={isActive ? wStatus?.currentAgentName : null}
511
+ errorMessage={workflowStatusValue === 'failed' ? wStatus?.error : null}
512
+ onClick={() => onCardClick({ id: card.id, name: card.name })}
513
+ onCopy={() => handleCopy(card)}
514
+ onPlay={() => card.nodeType === 'epic' ? startPlayAll(card.id) : handleStartWorkflow(card.id)}
515
+ onPlayDropdown={() => handlePlayDropdown(card.id)}
516
+ onStop={stopPlayAll}
517
+ onResume={() => handleResumeWorkflow(card.id)}
518
+ onIssuesClick={() => onIssuesClick(card.id, card.name, card.column, card.notes, card.reviews)}
519
+ onMonitorClick={() => setMonitorTarget({ featureId: card.id, featureName: card.name })}
520
+ draggable
521
+ onDragStart={(e) => handleDragStart(e, card.id)}
522
+ onDragEnd={handleDragEnd}
523
+ />
524
+ {/* Workflow selection dropdown — filtered by feature type */}
525
+ {dropdownOpen === card.id && (() => {
526
+ const cardFeatureType = card.featureType || 'code';
527
+ const filteredWorkflows = availableWorkflows.filter(wf =>
528
+ (!wf.featureType || wf.featureType === cardFeatureType)
529
+ && (!wf.triggerColumn || wf.triggerColumn === card.column)
530
+ );
531
+ return (
532
+ <div
533
+ className="absolute right-0 top-8 z-50 min-w-[180px] rounded-lg border border-edge bg-surface-raised shadow-xl shadow-black/20 py-1"
534
+ onClick={(e) => e.stopPropagation()}
535
+ >
536
+ {filteredWorkflows.map(wf => (
537
+ <button
538
+ key={wf.id}
539
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] text-content hover:bg-white/[0.08] transition-colors cursor-pointer"
540
+ onClick={() => {
541
+ setDropdownOpen(null);
542
+ handleStartWorkflow(card.id, wf.id);
543
+ }}
544
+ >
545
+ <span className="flex-1">{wf.name}</span>
546
+ {wf.isDefault === 1 && (
547
+ <span className="rounded-full bg-accent/15 px-1.5 py-0.5 text-[9px] font-bold text-accent">Default</span>
548
+ )}
549
+ </button>
550
+ ))}
551
+ </div>
552
+ );
553
+ })()}
554
+ </div>
478
555
  );
479
556
  })}
480
557
  </div>
@@ -482,6 +559,25 @@ export function KanbanView({ graphData, projectId, onCardClick, onIssuesClick }:
482
559
  );
483
560
  })}
484
561
 
562
+ {/* Workflow Monitor Modal */}
563
+ {monitorTarget && (
564
+ <WorkflowMonitorModal
565
+ featureId={monitorTarget.featureId}
566
+ featureName={monitorTarget.featureName}
567
+ isOpen={true}
568
+ onClose={() => setMonitorTarget(null)}
569
+ />
570
+ )}
571
+
572
+ {/* Iteration Comment Modal — backward card moves */}
573
+ <IterationCommentModal
574
+ isOpen={!!backwardMove}
575
+ featureName={backwardMove?.featureName || ''}
576
+ fromColumn={backwardMove?.fromColumn || ''}
577
+ toColumn={backwardMove?.toColumn || ''}
578
+ onConfirm={handleBackwardMoveConfirm}
579
+ onCancel={handleBackwardMoveCancel}
580
+ />
485
581
  </div>
486
582
  );
487
583
  }