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