@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
@@ -0,0 +1,103 @@
1
+ import type { Node, Edge } from '@xyflow/react';
2
+
3
+ const NODE_WIDTH = 200;
4
+ const NODE_HEIGHT = 160;
5
+ const HORIZONTAL_GAP = 60;
6
+ const VERTICAL_GAP = 80;
7
+
8
+ /**
9
+ * Auto-layout nodes as a top-down tree using BFS from root nodes.
10
+ * Nodes at the same depth are placed in the same row.
11
+ * Handles DAGs with multiple parents (node goes to the deepest level).
12
+ * Back-edges (cycles) are ignored during layout.
13
+ */
14
+ export function autoLayoutNodes(nodes: Node[], edges: Edge[]): Node[] {
15
+ if (nodes.length === 0) return nodes;
16
+
17
+ // Build adjacency list (source -> targets)
18
+ const children = new Map<string, string[]>();
19
+ const parentCount = new Map<string, number>();
20
+ for (const node of nodes) {
21
+ children.set(node.id, []);
22
+ parentCount.set(node.id, 0);
23
+ }
24
+ for (const edge of edges) {
25
+ if (children.has(edge.source) && parentCount.has(edge.target)) {
26
+ children.get(edge.source)!.push(edge.target);
27
+ parentCount.set(edge.target, (parentCount.get(edge.target) || 0) + 1);
28
+ }
29
+ }
30
+
31
+ // Find root nodes (no incoming edges) — typically the start node
32
+ const roots = nodes.filter((n) => (parentCount.get(n.id) || 0) === 0);
33
+ if (roots.length === 0) {
34
+ // Fallback: use start-type nodes, or first node
35
+ const startNodes = nodes.filter((n) => n.type === 'start');
36
+ roots.push(...(startNodes.length > 0 ? startNodes : [nodes[0]]));
37
+ }
38
+
39
+ // BFS to assign depth levels — a node's depth is the longest path from any root.
40
+ // Back-edges (cycles) are detected and skipped to prevent infinite loops.
41
+ const depth = new Map<string, number>();
42
+ const enqueued = new Map<string, number>(); // track how many times a node was enqueued
43
+ const maxDepth = nodes.length; // no valid path can exceed the number of nodes
44
+ const queue: Array<{ id: string; d: number }> = [];
45
+ for (const root of roots) {
46
+ depth.set(root.id, 0);
47
+ enqueued.set(root.id, 1);
48
+ queue.push({ id: root.id, d: 0 });
49
+ }
50
+
51
+ while (queue.length > 0) {
52
+ const { id, d } = queue.shift()!;
53
+ for (const childId of children.get(id) || []) {
54
+ const newDepth = d + 1;
55
+ // Skip back-edges that would exceed the maximum possible depth
56
+ if (newDepth > maxDepth) continue;
57
+ const currentDepth = depth.get(childId);
58
+ // Use the maximum depth (longest path) to handle DAGs
59
+ if (currentDepth === undefined || newDepth > currentDepth) {
60
+ depth.set(childId, newDepth);
61
+ queue.push({ id: childId, d: newDepth });
62
+ }
63
+ }
64
+ }
65
+
66
+ // Assign any unvisited nodes (disconnected) to depth 0
67
+ for (const node of nodes) {
68
+ if (!depth.has(node.id)) {
69
+ depth.set(node.id, 0);
70
+ }
71
+ }
72
+
73
+ // Group nodes by depth level
74
+ const levels = new Map<number, Node[]>();
75
+ for (const node of nodes) {
76
+ const d = depth.get(node.id)!;
77
+ if (!levels.has(d)) levels.set(d, []);
78
+ levels.get(d)!.push(node);
79
+ }
80
+
81
+ // Sort levels by depth
82
+ const sortedDepths = [...levels.keys()].sort((a, b) => a - b);
83
+
84
+ // Position nodes: center each row horizontally, stack rows vertically
85
+ const positioned = new Map<string, { x: number; y: number }>();
86
+ for (const d of sortedDepths) {
87
+ const levelNodes = levels.get(d)!;
88
+ const rowWidth = levelNodes.length * NODE_WIDTH + (levelNodes.length - 1) * HORIZONTAL_GAP;
89
+ const startX = -rowWidth / 2;
90
+
91
+ for (let i = 0; i < levelNodes.length; i++) {
92
+ positioned.set(levelNodes[i].id, {
93
+ x: startX + i * (NODE_WIDTH + HORIZONTAL_GAP),
94
+ y: d * (NODE_HEIGHT + VERTICAL_GAP),
95
+ });
96
+ }
97
+ }
98
+
99
+ return nodes.map((node) => ({
100
+ ...node,
101
+ position: positioned.get(node.id) || node.position,
102
+ }));
103
+ }
@@ -0,0 +1,35 @@
1
+ import type { Edge } from '@xyflow/react';
2
+
3
+ const EDGE_COLORS = [
4
+ '#b5e853', // lime green
5
+ '#f06292', // pink
6
+ '#4fc3f7', // sky blue
7
+ '#ffb74d', // orange
8
+ '#ce93d8', // purple
9
+ '#4db6ac', // teal
10
+ '#fff176', // yellow
11
+ '#e57373', // coral red
12
+ '#90a4ae', // blue grey
13
+ '#aed581', // light green
14
+ ];
15
+
16
+ /**
17
+ * Assigns a unique color to each outgoing edge of a node from a 10-color swatch.
18
+ * Edges from the same source get sequential colors so they're visually distinct.
19
+ */
20
+ export const colorizeEdges = (edges: Edge[]): Edge[] => {
21
+ // Group edges by source, preserving insertion order
22
+ const sourceIndex = new Map<string, number>();
23
+
24
+ return edges.map((edge) => {
25
+ const idx = sourceIndex.get(edge.source) ?? 0;
26
+ sourceIndex.set(edge.source, idx + 1);
27
+ const color = EDGE_COLORS[idx % EDGE_COLORS.length];
28
+
29
+ return {
30
+ ...edge,
31
+ style: { ...edge.style, stroke: color, strokeWidth: 2 },
32
+ markerEnd: { type: 'arrowclosed' as const, color },
33
+ };
34
+ });
35
+ };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Lightweight read-only node components for the Workflow Monitor.
3
+ * These re-render with execution state but are non-interactive (no editing).
4
+ * They include proper Handles so React Flow can render edges correctly.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { Handle, Position } from '@xyflow/react';
9
+ import type { NodeProps } from '@xyflow/react';
10
+ import {
11
+ Play, Square, ArrowLeftRight, Bot, MapPin, RefreshCw, Tag, Layers,
12
+ CircleDot, CheckCircle2, AlertTriangle, Clock, Package, Volume2, Film,
13
+ } from 'lucide-react';
14
+
15
+ /* ── Execution status styling ── */
16
+
17
+ const STATUS_RING: Record<string, string> = {
18
+ running: 'ring-2 ring-accent animate-pulse',
19
+ completed: 'ring-2 ring-emerald-400',
20
+ failed: 'ring-2 ring-error',
21
+ };
22
+
23
+ const STATUS_BG: Record<string, string> = {
24
+ running: 'bg-accent/10',
25
+ completed: 'bg-emerald-500/10',
26
+ failed: 'bg-error/10',
27
+ pending: 'bg-white/[0.03]',
28
+ };
29
+
30
+ function statusIcon(status: string, size = 14) {
31
+ if (status === 'running') return <CircleDot size={size} className="text-accent animate-pulse" />;
32
+ if (status === 'completed') return <CheckCircle2 size={size} className="text-emerald-400" />;
33
+ if (status === 'failed') return <AlertTriangle size={size} className="text-error" />;
34
+ return <Clock size={size} className="text-content-muted" />;
35
+ }
36
+
37
+ /* ── Base wrapper ── */
38
+
39
+ function MonitorNodeWrapper({
40
+ id,
41
+ data,
42
+ icon,
43
+ label,
44
+ detail,
45
+ hasSource = true,
46
+ hasTarget = true,
47
+ sourceHandles,
48
+ }: {
49
+ id: string;
50
+ data: Record<string, unknown>;
51
+ icon: React.ReactNode;
52
+ label: string;
53
+ detail?: string;
54
+ hasSource?: boolean;
55
+ hasTarget?: boolean;
56
+ sourceHandles?: string[];
57
+ }) {
58
+ const execStatus = (data._execStatus as string) || 'pending';
59
+ const error = data._error as string | null;
60
+ const onNodeClick = data._onNodeClick as ((nodeId: string) => void) | undefined;
61
+ const ringCls = STATUS_RING[execStatus] || '';
62
+ const bgCls = STATUS_BG[execStatus] || STATUS_BG.pending;
63
+
64
+ return (
65
+ <div
66
+ className={`px-4 py-3 rounded-lg border border-edge ${bgCls} ${ringCls} text-content min-w-[160px] cursor-pointer transition-all hover:border-content/30`}
67
+ onClick={(e) => { e.stopPropagation(); onNodeClick?.(id); }}
68
+ >
69
+ {hasTarget && (
70
+ <Handle type="target" position={Position.Top} className="!w-3 !h-3 !bg-accent !border-2 !border-surface" />
71
+ )}
72
+ <div className="flex items-center gap-2 mb-1">
73
+ {statusIcon(execStatus)}
74
+ {icon}
75
+ <span className="text-[11px] font-semibold uppercase tracking-wider">{label}</span>
76
+ </div>
77
+ {detail && (
78
+ <div className="text-[10px] text-content-secondary truncate">{detail}</div>
79
+ )}
80
+ {error && (
81
+ <div className="mt-1 flex items-center gap-1 text-[10px] text-error truncate">
82
+ <AlertTriangle size={10} />
83
+ <span className="truncate">{error}</span>
84
+ </div>
85
+ )}
86
+ {hasSource && !sourceHandles && (
87
+ <Handle type="source" position={Position.Bottom} className="!w-3 !h-3 !bg-accent !border-2 !border-surface" />
88
+ )}
89
+ {sourceHandles && sourceHandles.map(handle => (
90
+ <Handle
91
+ key={handle}
92
+ type="source"
93
+ position={Position.Bottom}
94
+ id={handle}
95
+ className="!w-3 !h-3 !bg-accent !border-2 !border-surface"
96
+ />
97
+ ))}
98
+ </div>
99
+ );
100
+ }
101
+
102
+ /* ── Specific node types ── */
103
+
104
+ export function MonitorStartNode({ id, data }: NodeProps) {
105
+ return (
106
+ <MonitorNodeWrapper
107
+ id={id}
108
+ data={data as Record<string, unknown>}
109
+ icon={<Play size={14} className="text-accent" />}
110
+ label="Start"
111
+ hasTarget={false}
112
+ />
113
+ );
114
+ }
115
+
116
+ export function MonitorEndNode({ id, data }: NodeProps) {
117
+ const outcome = (data.outcome as string) || (data.statusType as string) || 'success';
118
+ return (
119
+ <MonitorNodeWrapper
120
+ id={id}
121
+ data={data as Record<string, unknown>}
122
+ icon={<Square size={14} className={outcome === 'success' ? 'text-accent' : outcome === 'blocked' ? 'text-amber-400' : 'text-error'} />}
123
+ label="End"
124
+ detail={outcome}
125
+ hasSource={false}
126
+ />
127
+ );
128
+ }
129
+
130
+ export function MonitorTransitionCardNode({ id, data }: NodeProps) {
131
+ const d = data as Record<string, unknown>;
132
+ return (
133
+ <MonitorNodeWrapper
134
+ id={id}
135
+ data={d}
136
+ icon={<ArrowLeftRight size={14} className="text-blue-400" />}
137
+ label="Transition Card"
138
+ detail={`${d.fromColumn || '?'} \u2192 ${d.toColumn || '?'}`}
139
+ />
140
+ );
141
+ }
142
+
143
+ export function MonitorRunAgentNode({ id, data }: NodeProps) {
144
+ const d = data as Record<string, unknown>;
145
+ return (
146
+ <MonitorNodeWrapper
147
+ id={id}
148
+ data={d}
149
+ icon={<Bot size={14} className="text-purple-400" />}
150
+ label="Run Agent"
151
+ detail={(d.agentName as string) || (d.agentId as string) || ''}
152
+ />
153
+ );
154
+ }
155
+
156
+ export function MonitorCheckCardPositionNode({ id, data }: NodeProps) {
157
+ const d = data as Record<string, unknown>;
158
+ const outputs = (d.outputs as string[]) || ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'];
159
+ return (
160
+ <MonitorNodeWrapper
161
+ id={id}
162
+ data={d}
163
+ icon={<MapPin size={14} className="text-cyan-400" />}
164
+ label="Check Position"
165
+ sourceHandles={outputs}
166
+ />
167
+ );
168
+ }
169
+
170
+ export function MonitorCheckCycleCountNode({ id, data }: NodeProps) {
171
+ const d = data as Record<string, unknown>;
172
+ return (
173
+ <MonitorNodeWrapper
174
+ id={id}
175
+ data={d}
176
+ icon={<RefreshCw size={14} className="text-amber-400" />}
177
+ label="Check Cycles"
178
+ detail={`max: ${d.maxCycles || '?'}`}
179
+ sourceHandles={['under_limit', 'at_limit']}
180
+ />
181
+ );
182
+ }
183
+
184
+ export function MonitorSetCardMetadataNode({ id, data }: NodeProps) {
185
+ const d = data as Record<string, unknown>;
186
+ return (
187
+ <MonitorNodeWrapper
188
+ id={id}
189
+ data={d}
190
+ icon={<Tag size={14} className="text-teal-400" />}
191
+ label="Set Metadata"
192
+ detail={`${d.key || '?'} = ${d.value || '?'}`}
193
+ />
194
+ );
195
+ }
196
+
197
+ export function MonitorGroupNode({ id, data }: NodeProps) {
198
+ const d = data as Record<string, unknown>;
199
+ return (
200
+ <MonitorNodeWrapper
201
+ id={id}
202
+ data={d}
203
+ icon={<Layers size={14} className="text-indigo-400" />}
204
+ label="Group"
205
+ detail={(d.groupName as string) || ''}
206
+ />
207
+ );
208
+ }
209
+
210
+ export function MonitorRebuildBundleNode({ id, data }: NodeProps) {
211
+ return (
212
+ <MonitorNodeWrapper
213
+ id={id}
214
+ data={data as Record<string, unknown>}
215
+ icon={<Package size={14} className="text-orange-400" />}
216
+ label="Rebuild Bundle"
217
+ detail="Remotion webpack build"
218
+ />
219
+ );
220
+ }
221
+
222
+ export function MonitorGenerateTTSNode({ id, data }: NodeProps) {
223
+ const d = data as Record<string, unknown>;
224
+ return (
225
+ <MonitorNodeWrapper
226
+ id={id}
227
+ data={d}
228
+ icon={<Volume2 size={14} className="text-emerald-400" />}
229
+ label="Generate TTS"
230
+ detail={(d.scriptPath as string) || 'script.md'}
231
+ />
232
+ );
233
+ }
234
+
235
+ export function MonitorRenderVideoNode({ id, data }: NodeProps) {
236
+ const d = data as Record<string, unknown>;
237
+ return (
238
+ <MonitorNodeWrapper
239
+ id={id}
240
+ data={d}
241
+ icon={<Film size={14} className="text-rose-400" />}
242
+ label="Render Video"
243
+ detail={`${(d.compositionId as string) || 'auto'} @ ${(d.resolution as string) || '1920x1080'}`}
244
+ />
245
+ );
246
+ }
@@ -0,0 +1,119 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ WORKFLOW_NODE_TYPES,
5
+ KANBAN_COLUMNS,
6
+ END_STATUS_TYPES,
7
+ NODE_PALETTE_ITEMS,
8
+ NODE_DEFAULTS,
9
+ } from './workflow_types.ts';
10
+
11
+ describe('WORKFLOW_NODE_TYPES', () => {
12
+ it('contains all eight node types including group', () => {
13
+ assert.equal(WORKFLOW_NODE_TYPES.length, 8);
14
+ assert.ok(WORKFLOW_NODE_TYPES.includes('start'));
15
+ assert.ok(WORKFLOW_NODE_TYPES.includes('end'));
16
+ assert.ok(WORKFLOW_NODE_TYPES.includes('transitionCard'));
17
+ assert.ok(WORKFLOW_NODE_TYPES.includes('runAgent'));
18
+ assert.ok(WORKFLOW_NODE_TYPES.includes('checkCardPosition'));
19
+ assert.ok(WORKFLOW_NODE_TYPES.includes('checkCycleCount'));
20
+ assert.ok(WORKFLOW_NODE_TYPES.includes('setCardMetadata'));
21
+ assert.ok(WORKFLOW_NODE_TYPES.includes('group'));
22
+ });
23
+ });
24
+
25
+ describe('KANBAN_COLUMNS', () => {
26
+ it('contains the six kanban columns in order', () => {
27
+ assert.deepEqual([...KANBAN_COLUMNS], [
28
+ 'backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done',
29
+ ]);
30
+ });
31
+ });
32
+
33
+ describe('END_STATUS_TYPES', () => {
34
+ it('contains success, blocked, and failed', () => {
35
+ assert.deepEqual([...END_STATUS_TYPES], ['success', 'blocked', 'failed']);
36
+ });
37
+ });
38
+
39
+ describe('NODE_PALETTE_ITEMS', () => {
40
+ it('has entries for all non-group node types', () => {
41
+ // group is not in the palette (it's added via the Groups section)
42
+ const paletteTypes = NODE_PALETTE_ITEMS.map(item => item.type);
43
+ for (const nodeType of WORKFLOW_NODE_TYPES) {
44
+ if (nodeType === 'group') continue;
45
+ assert.ok(paletteTypes.includes(nodeType), `Missing palette item for ${nodeType}`);
46
+ }
47
+ });
48
+
49
+ it('each item has a non-empty label and description', () => {
50
+ for (const item of NODE_PALETTE_ITEMS) {
51
+ assert.ok(item.label.length > 0, `Empty label for ${item.type}`);
52
+ assert.ok(item.description.length > 0, `Empty description for ${item.type}`);
53
+ }
54
+ });
55
+ });
56
+
57
+ describe('NODE_DEFAULTS', () => {
58
+ it('has a default factory for every node type', () => {
59
+ for (const nodeType of WORKFLOW_NODE_TYPES) {
60
+ assert.ok(typeof NODE_DEFAULTS[nodeType] === 'function', `Missing default for ${nodeType}`);
61
+ }
62
+ });
63
+
64
+ it('start returns empty object', () => {
65
+ const result = NODE_DEFAULTS.start();
66
+ assert.deepEqual(result, {});
67
+ });
68
+
69
+ it('transitionCard returns default columns', () => {
70
+ const result = NODE_DEFAULTS.transitionCard();
71
+ assert.equal(result.sourceColumn, 'todo');
72
+ assert.equal(result.targetColumn, 'in_progress');
73
+ });
74
+
75
+ it('runAgent returns empty agentId and agentName', () => {
76
+ const result = NODE_DEFAULTS.runAgent();
77
+ assert.equal(result.agentId, '');
78
+ assert.equal(result.agentName, '');
79
+ });
80
+
81
+ it('checkCardPosition returns empty object', () => {
82
+ const result = NODE_DEFAULTS.checkCardPosition();
83
+ assert.deepEqual(result, {});
84
+ });
85
+
86
+ it('checkCycleCount defaults maxCount to 3', () => {
87
+ const result = NODE_DEFAULTS.checkCycleCount();
88
+ assert.equal(result.maxCount, 3);
89
+ });
90
+
91
+ it('setCardMetadata returns empty key and value', () => {
92
+ const result = NODE_DEFAULTS.setCardMetadata();
93
+ assert.equal(result.key, '');
94
+ assert.equal(result.value, '');
95
+ });
96
+
97
+ it('end defaults statusType to success', () => {
98
+ const result = NODE_DEFAULTS.end();
99
+ assert.equal(result.statusType, 'success');
100
+ });
101
+
102
+ it('group returns default group data', () => {
103
+ const result = NODE_DEFAULTS.group();
104
+ assert.equal(result.groupId, '');
105
+ assert.equal(result.groupName, '');
106
+ assert.ok(Array.isArray(result.internalNodes));
107
+ assert.ok(Array.isArray(result.internalEdges));
108
+ assert.ok(Array.isArray(result.inputHandles));
109
+ assert.ok(Array.isArray(result.outputHandles));
110
+ });
111
+
112
+ it('each factory returns a new object on each call (no shared state)', () => {
113
+ for (const nodeType of WORKFLOW_NODE_TYPES) {
114
+ const a = NODE_DEFAULTS[nodeType]();
115
+ const b = NODE_DEFAULTS[nodeType]();
116
+ assert.notEqual(a, b, `${nodeType} factory returns same object reference`);
117
+ }
118
+ });
119
+ });
@@ -0,0 +1,136 @@
1
+ import type { Node, Edge } from '@xyflow/react';
2
+
3
+ export const WORKFLOW_NODE_TYPES = [
4
+ 'start',
5
+ 'transitionCard',
6
+ 'runAgent',
7
+ 'checkCardPosition',
8
+ 'checkCycleCount',
9
+ 'setCardMetadata',
10
+ 'end',
11
+ 'group',
12
+ 'rebuildBundle',
13
+ 'generateTTS',
14
+ 'renderVideo',
15
+ ] as const;
16
+
17
+ export type WorkflowNodeType = typeof WORKFLOW_NODE_TYPES[number];
18
+
19
+ export const KANBAN_COLUMNS = [
20
+ 'backlog',
21
+ 'todo',
22
+ 'in_progress',
23
+ 'in_review',
24
+ 'qa',
25
+ 'done',
26
+ ] as const;
27
+
28
+ export type KanbanColumn = typeof KANBAN_COLUMNS[number];
29
+
30
+ export const END_STATUS_TYPES = ['success', 'blocked', 'failed'] as const;
31
+ export type EndStatusType = typeof END_STATUS_TYPES[number];
32
+
33
+ export interface TransitionCardData {
34
+ sourceColumn: KanbanColumn;
35
+ targetColumn: KanbanColumn;
36
+ }
37
+
38
+ export interface RunAgentData {
39
+ agentId: string;
40
+ agentName: string;
41
+ }
42
+
43
+ export interface CheckCardPositionData {
44
+ // no config needed — outputs are per-column handles
45
+ }
46
+
47
+ export interface CheckCycleCountData {
48
+ maxCount: number;
49
+ }
50
+
51
+ export interface SetCardMetadataData {
52
+ key: string;
53
+ value: string;
54
+ }
55
+
56
+ export interface EndNodeData {
57
+ statusType: EndStatusType;
58
+ }
59
+
60
+ export interface StartNodeData {
61
+ // no config
62
+ }
63
+
64
+ export interface GroupNodeData {
65
+ groupId: string;
66
+ groupName: string;
67
+ /** The internal nodes/edges of this group instance (may diverge from saved definition) */
68
+ internalNodes: Node[];
69
+ internalEdges: Edge[];
70
+ /** Handle IDs for external connections */
71
+ inputHandles: string[];
72
+ outputHandles: string[];
73
+ }
74
+
75
+ export interface RebuildBundleData {
76
+ // no config needed — triggers Remotion webpack bundler
77
+ }
78
+
79
+ export interface GenerateTTSData {
80
+ scriptPath: string;
81
+ force: boolean;
82
+ voiceId: string;
83
+ }
84
+
85
+ export interface RenderVideoData {
86
+ compositionId: string;
87
+ resolution: string;
88
+ aspectRatio: string;
89
+ fileOutputPrefix: string;
90
+ }
91
+
92
+ export type WorkflowNodeData =
93
+ | ({ type: 'start' } & StartNodeData)
94
+ | ({ type: 'transitionCard' } & TransitionCardData)
95
+ | ({ type: 'runAgent' } & RunAgentData)
96
+ | ({ type: 'checkCardPosition' } & CheckCardPositionData)
97
+ | ({ type: 'checkCycleCount' } & CheckCycleCountData)
98
+ | ({ type: 'setCardMetadata' } & SetCardMetadataData)
99
+ | ({ type: 'end' } & EndNodeData)
100
+ | ({ type: 'group' } & GroupNodeData)
101
+ | ({ type: 'rebuildBundle' } & RebuildBundleData)
102
+ | ({ type: 'generateTTS' } & GenerateTTSData)
103
+ | ({ type: 'renderVideo' } & RenderVideoData);
104
+
105
+ export interface NodePaletteItem {
106
+ type: WorkflowNodeType;
107
+ label: string;
108
+ description: string;
109
+ }
110
+
111
+ export const NODE_PALETTE_ITEMS: NodePaletteItem[] = [
112
+ { type: 'start', label: 'Start', description: 'Workflow entry point' },
113
+ { type: 'transitionCard', label: 'Transition Card', description: 'Move card between columns' },
114
+ { type: 'runAgent', label: 'Run Agent', description: 'Execute an AI agent' },
115
+ { type: 'checkCardPosition', label: 'Check Card Position', description: 'Branch by card column' },
116
+ { type: 'checkCycleCount', label: 'Check Cycle Count', description: 'Branch by iteration count' },
117
+ { type: 'setCardMetadata', label: 'Set Card Metadata', description: 'Set key/value on card' },
118
+ { type: 'end', label: 'End', description: 'Workflow exit point' },
119
+ { type: 'rebuildBundle', label: 'Rebuild Bundle', description: 'Build Remotion webpack bundle' },
120
+ { type: 'generateTTS', label: 'Generate TTS', description: 'Generate text-to-speech audio' },
121
+ { type: 'renderVideo', label: 'Render Video', description: 'Render composition to MP4' },
122
+ ];
123
+
124
+ export const NODE_DEFAULTS: Record<string, () => Record<string, unknown>> = {
125
+ start: () => ({}),
126
+ transitionCard: () => ({ sourceColumn: 'todo', targetColumn: 'in_progress' }),
127
+ runAgent: () => ({ agentId: '', agentName: '' }),
128
+ checkCardPosition: () => ({}),
129
+ checkCycleCount: () => ({ maxCount: 3 }),
130
+ setCardMetadata: () => ({ key: '', value: '' }),
131
+ end: () => ({ statusType: 'success' }),
132
+ group: () => ({ groupId: '', groupName: '', internalNodes: [], internalEdges: [], inputHandles: [], outputHandles: [] }),
133
+ rebuildBundle: () => ({}),
134
+ generateTTS: () => ({ scriptPath: '', force: false, voiceId: '' }),
135
+ renderVideo: () => ({ compositionId: '', resolution: '1920x1080', aspectRatio: '', fileOutputPrefix: '' }),
136
+ };