@assistkick/create 1.9.0 → 1.11.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/src/scaffolder.d.ts +12 -1
  2. package/dist/src/scaffolder.js +40 -3
  3. package/dist/src/scaffolder.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/assistkick-product-system/package.json +1 -1
  6. package/templates/assistkick-product-system/packages/backend/package.json +1 -0
  7. package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
  9. package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
  10. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
  13. package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
  14. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
  15. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
  17. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
  18. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +391 -23
  19. package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
  20. package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
  21. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
  22. package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
  23. package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
  24. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
  25. package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
  26. package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
  27. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
  28. package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
  29. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
  30. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
  31. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
  32. package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
  33. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
  34. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
  35. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
  41. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
  42. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
  43. package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
  44. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
  45. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
  46. package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
  47. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
  48. package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
  49. package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
  50. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
  51. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
  52. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
  53. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
  54. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
  55. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
  56. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
  57. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
  58. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
  59. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
  60. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
  61. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +149 -14
  62. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
  63. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
  64. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
  65. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
  66. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
  67. package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
  68. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
  69. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
  70. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
  71. package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
  85. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
  86. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
  87. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
  88. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
  89. package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
  92. package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
  97. package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
  99. package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
  102. package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
  106. package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
  109. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
  111. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
  112. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
  113. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
  114. package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
  134. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
  138. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
  139. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
  140. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
  141. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
  142. package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
  143. package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
  144. package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
  145. package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
  146. package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
  147. package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
  148. package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
  149. package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
  150. package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
  151. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
  152. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
  153. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
  154. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
  155. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
  156. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
  157. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  179. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  180. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  181. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  182. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  183. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  185. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  186. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  187. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  188. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  189. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  190. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  191. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  192. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  193. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  194. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  195. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  196. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  197. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  198. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  199. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  200. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  201. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  202. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  203. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  204. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  205. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  206. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -22,6 +22,7 @@ import { RunAgentNode } from './RunAgentNode';
22
22
  import { CheckCardPositionNode } from './CheckCardPositionNode';
23
23
  import { CheckCycleCountNode } from './CheckCycleCountNode';
24
24
  import { SetCardMetadataNode } from './SetCardMetadataNode';
25
+ import { ProgrammableNode } from './ProgrammableNode';
25
26
 
26
27
  /** Node types available inside the group sub-canvas (no nested groups). */
27
28
  const INNER_NODE_TYPE_MAP: NodeTypes = {
@@ -32,6 +33,7 @@ const INNER_NODE_TYPE_MAP: NodeTypes = {
32
33
  checkCardPosition: CheckCardPositionNode,
33
34
  checkCycleCount: CheckCycleCountNode,
34
35
  setCardMetadata: SetCardMetadataNode,
36
+ programmable: ProgrammableNode,
35
37
  };
36
38
 
37
39
  function GroupSubCanvas({
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
2
  import {
3
- Play, Square, ArrowRightLeft, Bot, GitBranch, RefreshCw, Tag, Layers, Trash2,
3
+ Play, Square, ArrowRightLeft, Bot, GitBranch, RefreshCw, Tag, Layers, Trash2, Code,
4
4
  } from 'lucide-react';
5
5
  import { NODE_PALETTE_ITEMS } from './workflow_types';
6
6
  import type { WorkflowNodeType } from './workflow_types';
@@ -14,6 +14,7 @@ const NODE_ICONS: Record<string, React.ReactNode> = {
14
14
  checkCardPosition: <GitBranch size={14} className="text-cyan-400" />,
15
15
  checkCycleCount: <RefreshCw size={14} className="text-orange-400" />,
16
16
  setCardMetadata: <Tag size={14} className="text-yellow-400" />,
17
+ programmable: <Code size={14} className="text-emerald-400" />,
17
18
  };
18
19
 
19
20
  interface WorkflowGroup {
@@ -0,0 +1,178 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Handle, Position, NodeResizer, useReactFlow } from '@xyflow/react';
3
+ import type { NodeProps } from '@xyflow/react';
4
+ import { Code, ChevronDown, ChevronRight } from 'lucide-react';
5
+ import Editor from '@monaco-editor/react';
6
+ import { useTheme } from '../../hooks/useTheme';
7
+
8
+ const MIN_WIDTH = 340;
9
+ const MIN_HEIGHT = 320;
10
+
11
+ export function ProgrammableNode({ id, data, selected }: NodeProps) {
12
+ const { updateNodeData } = useReactFlow();
13
+ const { theme } = useTheme();
14
+ const [limitsExpanded, setLimitsExpanded] = useState(false);
15
+
16
+ const label = (data.label as string) || '';
17
+ const code = (data.code as string) || '';
18
+ const timeout = (data.timeout as number) || 30000;
19
+ const memory = (data.memory as number) || 128;
20
+ const maxRequests = (data.maxRequests as number) || 20;
21
+ const maxResponseSize = (data.maxResponseSize as number) || 5242880;
22
+
23
+ const handleLabelChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
24
+ updateNodeData(id, { label: e.target.value });
25
+ }, [id, updateNodeData]);
26
+
27
+ const handleCodeChange = useCallback((value: string | undefined) => {
28
+ if (value !== undefined) {
29
+ updateNodeData(id, { code: value });
30
+ }
31
+ }, [id, updateNodeData]);
32
+
33
+ const handleLimitChange = useCallback((field: string, value: number) => {
34
+ updateNodeData(id, { [field]: value });
35
+ }, [id, updateNodeData]);
36
+
37
+ return (
38
+ <div className={`rounded-lg border bg-surface-raised text-content flex flex-col h-full ${
39
+ selected ? 'border-accent shadow-[0_0_0_1px_var(--accent)]' : 'border-edge'
40
+ }`}>
41
+ <NodeResizer
42
+ isVisible={selected}
43
+ minWidth={MIN_WIDTH}
44
+ minHeight={MIN_HEIGHT}
45
+ lineClassName="!border-accent"
46
+ handleClassName="!w-2 !h-2 !bg-accent !border-2 !border-surface !rounded-sm"
47
+ />
48
+ <Handle type="target" position={Position.Top} className="!w-3 !h-3 !bg-accent !border-2 !border-surface" />
49
+
50
+ {/* Header */}
51
+ <div className="flex items-center gap-2 px-4 pt-3 pb-2">
52
+ <Code size={14} className="text-emerald-400" />
53
+ <span className="text-[12px] font-semibold">Programmable</span>
54
+ </div>
55
+
56
+ {/* Label / Node Name */}
57
+ <div className="px-4 pb-2">
58
+ <label className="text-[10px] text-content-muted uppercase tracking-wider">Node Name</label>
59
+ <input
60
+ type="text"
61
+ value={label}
62
+ onChange={handleLabelChange}
63
+ placeholder="e.g. Validate Webhook"
64
+ className="w-full px-2 py-1 rounded border border-edge bg-surface text-[11px] text-content"
65
+ />
66
+ </div>
67
+
68
+ {/* Monaco Editor — fills remaining vertical space */}
69
+ <div className="px-4 pb-2 flex-1 min-h-0 flex flex-col">
70
+ <label className="text-[10px] text-content-muted uppercase tracking-wider">Code</label>
71
+ <div className="border border-edge rounded overflow-hidden flex-1 min-h-[120px]">
72
+ <Editor
73
+ height="100%"
74
+ language="javascript"
75
+ theme={theme === 'dark' ? 'vs-dark' : 'vs'}
76
+ value={code}
77
+ onChange={handleCodeChange}
78
+ options={{
79
+ minimap: { enabled: false },
80
+ fontSize: 11,
81
+ lineNumbers: 'on',
82
+ scrollBeyondLastLine: false,
83
+ wordWrap: 'on',
84
+ tabSize: 2,
85
+ automaticLayout: true,
86
+ overviewRulerLanes: 0,
87
+ hideCursorInOverviewRuler: true,
88
+ scrollbar: { vertical: 'auto', horizontal: 'auto' },
89
+ }}
90
+ />
91
+ </div>
92
+ </div>
93
+
94
+ {/* Execution Limits (collapsible) */}
95
+ <div className="px-4 pb-3 shrink-0">
96
+ <button
97
+ onClick={() => setLimitsExpanded(!limitsExpanded)}
98
+ className="flex items-center gap-1 text-[10px] text-content-muted uppercase tracking-wider hover:text-content transition-colors"
99
+ >
100
+ {limitsExpanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
101
+ Execution Limits
102
+ </button>
103
+
104
+ {limitsExpanded && (
105
+ <div className="grid grid-cols-2 gap-2 mt-2">
106
+ <div>
107
+ <label className="text-[9px] text-content-muted">Timeout (ms)</label>
108
+ <input
109
+ type="number"
110
+ min={1000}
111
+ step={1000}
112
+ value={timeout}
113
+ onChange={(e) => handleLimitChange('timeout', parseInt(e.target.value, 10) || 30000)}
114
+ className="w-full px-2 py-1 rounded border border-edge bg-surface text-[10px] text-content"
115
+ />
116
+ </div>
117
+ <div>
118
+ <label className="text-[9px] text-content-muted">Memory (MB)</label>
119
+ <input
120
+ type="number"
121
+ min={8}
122
+ step={8}
123
+ value={memory}
124
+ onChange={(e) => handleLimitChange('memory', parseInt(e.target.value, 10) || 128)}
125
+ className="w-full px-2 py-1 rounded border border-edge bg-surface text-[10px] text-content"
126
+ />
127
+ </div>
128
+ <div>
129
+ <label className="text-[9px] text-content-muted">Max Requests</label>
130
+ <input
131
+ type="number"
132
+ min={0}
133
+ value={maxRequests}
134
+ onChange={(e) => handleLimitChange('maxRequests', parseInt(e.target.value, 10) || 20)}
135
+ className="w-full px-2 py-1 rounded border border-edge bg-surface text-[10px] text-content"
136
+ />
137
+ </div>
138
+ <div>
139
+ <label className="text-[9px] text-content-muted">Max Response (bytes)</label>
140
+ <input
141
+ type="number"
142
+ min={1024}
143
+ step={1024}
144
+ value={maxResponseSize}
145
+ onChange={(e) => handleLimitChange('maxResponseSize', parseInt(e.target.value, 10) || 5242880)}
146
+ className="w-full px-2 py-1 rounded border border-edge bg-surface text-[10px] text-content"
147
+ />
148
+ </div>
149
+ </div>
150
+ )}
151
+ </div>
152
+
153
+ {/* Output handles: success and error */}
154
+ <div className="flex justify-between px-4 pb-3 text-[9px] text-content-muted shrink-0">
155
+ <div className="relative">
156
+ <span className="px-1.5 py-0.5 rounded bg-surface border border-edge">success</span>
157
+ <Handle
158
+ type="source"
159
+ position={Position.Bottom}
160
+ id="success"
161
+ className="!w-2 !h-2 !bg-green-400 !border !border-surface"
162
+ style={{ left: '30%' }}
163
+ />
164
+ </div>
165
+ <div className="relative">
166
+ <span className="px-1.5 py-0.5 rounded bg-surface border border-edge">error</span>
167
+ <Handle
168
+ type="source"
169
+ position={Position.Bottom}
170
+ id="error"
171
+ className="!w-2 !h-2 !bg-red-400 !border !border-surface"
172
+ style={{ left: '70%' }}
173
+ />
174
+ </div>
175
+ </div>
176
+ </div>
177
+ );
178
+ }
@@ -30,6 +30,7 @@ import { GroupNode } from './GroupNode';
30
30
  import { RebuildBundleNode } from './RebuildBundleNode';
31
31
  import { GenerateTTSNode } from './GenerateTTSNode';
32
32
  import { RenderVideoNode } from './RenderVideoNode';
33
+ import { ProgrammableNode } from './ProgrammableNode';
33
34
  import { NODE_DEFAULTS } from './workflow_types';
34
35
  import type { WorkflowNodeType } from './workflow_types';
35
36
  import { autoLayoutNodes } from './autoLayout';
@@ -47,6 +48,7 @@ const NODE_TYPE_MAP: NodeTypes = {
47
48
  rebuildBundle: RebuildBundleNode,
48
49
  generateTTS: GenerateTTSNode,
49
50
  renderVideo: RenderVideoNode,
51
+ programmable: ProgrammableNode,
50
52
  };
51
53
 
52
54
  interface ContextMenuState {
@@ -168,6 +170,7 @@ export function WorkflowCanvasInner({
168
170
  type,
169
171
  position,
170
172
  data: defaults(),
173
+ ...(type === 'programmable' ? { style: { width: 380, height: 400 } } : {}),
171
174
  };
172
175
 
173
176
  setNodes((nds) => [...nds, newNode]);
@@ -11,9 +11,10 @@ import {
11
11
  } from '@xyflow/react';
12
12
  import '@xyflow/react/dist/style.css';
13
13
  import {
14
- X, Clock, AlertTriangle, CheckCircle2, CircleDot,
14
+ X, Clock, AlertTriangle, CheckCircle2, CircleDot, Square,
15
15
  FileText, Pencil, Terminal, Search, FolderOpen, Bot,
16
16
  MessageSquare, ChevronDown, ChevronRight, SkipForward, RotateCcw,
17
+ Play,
17
18
  } from 'lucide-react';
18
19
  import { apiClient, type WorkflowMonitorData, type NodeExecutionData, type ToolCallEntry } from '../../api/client';
19
20
  import {
@@ -28,6 +29,7 @@ import {
28
29
  MonitorRebuildBundleNode,
29
30
  MonitorGenerateTTSNode,
30
31
  MonitorRenderVideoNode,
32
+ MonitorProgrammableNode,
31
33
  } from './monitor_nodes';
32
34
  import { colorizeEdges } from './edgeColors';
33
35
 
@@ -119,6 +121,7 @@ const MONITOR_NODE_TYPES: NodeTypes = {
119
121
  rebuildBundle: MonitorRebuildBundleNode,
120
122
  generateTTS: MonitorGenerateTTSNode,
121
123
  renderVideo: MonitorRenderVideoNode,
124
+ programmable: MonitorProgrammableNode,
122
125
  };
123
126
 
124
127
  function getNodeTypeLabel(type: string): string {
@@ -134,6 +137,7 @@ function getNodeTypeLabel(type: string): string {
134
137
  case 'rebuildBundle': return 'Rebuild Bundle';
135
138
  case 'generateTTS': return 'Generate TTS';
136
139
  case 'renderVideo': return 'Render Video';
140
+ case 'programmable': return 'Programmable';
137
141
  default: return type;
138
142
  }
139
143
  }
@@ -146,6 +150,7 @@ function getNodeLabel(type: string, data: Record<string, unknown>): string {
146
150
  case 'setCardMetadata': return `${data.key || '?'} = ${data.value || '?'}`;
147
151
  case 'end': return (data.outcome as string) || (data.statusType as string) || '';
148
152
  case 'group': return (data.groupName as string) || '';
153
+ case 'programmable': return (data.label as string) || '';
149
154
  default: return '';
150
155
  }
151
156
  }
@@ -154,6 +159,7 @@ function getNodeIcon(type: string, status: string): React.ReactNode {
154
159
  if (status === 'running') return <CircleDot size={14} className="text-accent animate-pulse" />;
155
160
  if (status === 'completed') return <CheckCircle2 size={14} className="text-emerald-400" />;
156
161
  if (status === 'failed') return <AlertTriangle size={14} className="text-error" />;
162
+ if (status === 'stopped') return <Square size={14} className="text-error" />;
157
163
  switch (type) {
158
164
  case 'runAgent': return <Bot size={14} className="text-purple-400" />;
159
165
  default: return <Clock size={14} className="text-content-muted" />;
@@ -170,6 +176,7 @@ function formatContextWindow(value: number): string {
170
176
  function StatusBadge({ status }: { status: string }) {
171
177
  const cls = status === 'completed' ? 'bg-emerald-500/15 text-emerald-400'
172
178
  : status === 'failed' ? 'bg-error/15 text-error'
179
+ : status === 'stopped' ? 'bg-error/15 text-error'
173
180
  : status === 'running' ? 'bg-accent/15 text-accent animate-pulse'
174
181
  : 'bg-white/[0.06] text-content-muted';
175
182
  return (
@@ -195,7 +202,7 @@ function NodeDetailPanel({
195
202
  nodeId,
196
203
  nodeType,
197
204
  nodeData,
198
- execution,
205
+ executions,
199
206
  isCurrentNode,
200
207
  featureId,
201
208
  onClose,
@@ -204,18 +211,50 @@ function NodeDetailPanel({
204
211
  nodeId: string;
205
212
  nodeType: string;
206
213
  nodeData: Record<string, unknown>;
207
- execution: NodeExecutionData | null;
214
+ executions: NodeExecutionData[];
208
215
  isCurrentNode: boolean;
209
216
  featureId: string;
210
217
  onClose: () => void;
211
218
  onAction: () => void;
212
219
  }) {
213
220
  const toolCallsRef = useRef<HTMLDivElement>(null);
221
+ const hasInitialScrolled = useRef(false);
214
222
  const [actionLoading, setActionLoading] = useState<string | null>(null);
223
+ const hasCycles = executions.length > 1;
224
+ const [selectedCycle, setSelectedCycle] = useState<number>(executions.length > 0 ? executions[executions.length - 1].cycle : 1);
225
+
226
+ // Update selected cycle when executions change (new cycle arrives)
227
+ useEffect(() => {
228
+ if (executions.length > 0) {
229
+ const latestCycle = executions[executions.length - 1].cycle;
230
+ setSelectedCycle(latestCycle);
231
+ }
232
+ }, [executions.length]);
233
+
234
+ const execution = executions.find(e => e.cycle === selectedCycle) ?? executions[executions.length - 1] ?? null;
215
235
  const statusLabel = execution?.status || 'pending';
216
236
  const isAgent = nodeType === 'runAgent';
217
237
  const outputData = execution?.outputData;
218
- const showActions = isCurrentNode && (statusLabel === 'running' || statusLabel === 'failed');
238
+ const showActions = isCurrentNode && (statusLabel === 'running' || statusLabel === 'failed' || statusLabel === 'stopped');
239
+ const showContinue = isCurrentNode && isAgent && execution?.claudeSessionId &&
240
+ (statusLabel === 'failed' || statusLabel === 'stopped' || (statusLabel === 'running' && !execution?.processAlive));
241
+
242
+ // Auto-scroll activity log to bottom on initial open only
243
+ useEffect(() => {
244
+ if (!hasInitialScrolled.current && toolCallsRef.current && execution?.toolCalls && execution.toolCalls.length > 0) {
245
+ toolCallsRef.current.scrollTop = toolCallsRef.current.scrollHeight;
246
+ hasInitialScrolled.current = true;
247
+ }
248
+ });
249
+
250
+ const handleStop = async () => {
251
+ setActionLoading('stop');
252
+ try {
253
+ await apiClient.stopNode(featureId, nodeId);
254
+ onAction();
255
+ } catch { /* error is non-critical — polling will pick up the state */ }
256
+ setActionLoading(null);
257
+ };
219
258
 
220
259
  const handleForceNext = async () => {
221
260
  setActionLoading('force-next');
@@ -235,6 +274,15 @@ function NodeDetailPanel({
235
274
  setActionLoading(null);
236
275
  };
237
276
 
277
+ const handleContinue = async () => {
278
+ setActionLoading('continue');
279
+ try {
280
+ await apiClient.continueNode(featureId, nodeId);
281
+ onAction();
282
+ } catch { /* error is non-critical — polling will pick up the state */ }
283
+ setActionLoading(null);
284
+ };
285
+
238
286
  return (
239
287
  <div className="w-1/2 shrink-0 border-l border-edge bg-surface flex flex-col overflow-hidden">
240
288
  {/* Header */}
@@ -247,6 +295,28 @@ function NodeDetailPanel({
247
295
  <StatusBadge status={statusLabel} />
248
296
  {showActions && (
249
297
  <div className="flex items-center gap-1.5 ml-2">
298
+ {statusLabel === 'running' && (
299
+ <button
300
+ onClick={handleStop}
301
+ disabled={actionLoading !== null}
302
+ className="flex items-center gap-1 bg-transparent border border-error/40 text-error rounded px-2 py-1 text-[11px] cursor-pointer font-mono hover:bg-error/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
303
+ title="Stop this node"
304
+ >
305
+ <Square size={11} />
306
+ {actionLoading === 'stop' ? 'Stopping...' : 'Stop'}
307
+ </button>
308
+ )}
309
+ {showContinue && (
310
+ <button
311
+ onClick={handleContinue}
312
+ disabled={actionLoading !== null}
313
+ className="flex items-center gap-1 bg-transparent border border-emerald-400/40 text-emerald-400 rounded px-2 py-1 text-[11px] cursor-pointer font-mono hover:bg-emerald-400/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
314
+ title="Resume Claude session"
315
+ >
316
+ <Play size={11} />
317
+ {actionLoading === 'continue' ? 'Continuing...' : 'Continue'}
318
+ </button>
319
+ )}
250
320
  <button
251
321
  onClick={handleRestart}
252
322
  disabled={actionLoading !== null}
@@ -276,6 +346,26 @@ function NodeDetailPanel({
276
346
  </button>
277
347
  </div>
278
348
 
349
+ {/* Cycle tabs — only shown when multiple cycles exist */}
350
+ {hasCycles && (
351
+ <div className="flex items-center gap-1 px-6 py-2 border-b border-edge bg-surface-alt">
352
+ <span className="text-[10px] font-bold uppercase tracking-widest text-content-muted mr-2">Cycle</span>
353
+ {executions.map(ex => (
354
+ <button
355
+ key={ex.cycle}
356
+ onClick={() => setSelectedCycle(ex.cycle)}
357
+ className={`px-2.5 py-1 rounded text-[11px] font-mono font-semibold cursor-pointer transition-colors ${
358
+ selectedCycle === ex.cycle
359
+ ? 'bg-accent/20 text-accent border border-accent/40'
360
+ : 'bg-white/[0.04] text-content-muted border border-edge hover:bg-white/[0.08] hover:text-content'
361
+ }`}
362
+ >
363
+ {ex.cycle}
364
+ </button>
365
+ ))}
366
+ </div>
367
+ )}
368
+
279
369
  {/* Content */}
280
370
  <div className="flex-1 overflow-y-auto px-6 py-5 space-y-5">
281
371
  {/* Timing */}
@@ -437,15 +527,18 @@ function MonitorGraphView({
437
527
  // Convert graph data to React Flow nodes/edges with execution state
438
528
  const nodes: Node[] = useMemo(() =>
439
529
  monitorData.graphData.nodes.map(n => {
440
- const exec = monitorData.nodeExecutions[n.id];
530
+ const executions = monitorData.nodeExecutions[n.id];
531
+ const latestExec = executions?.[executions.length - 1] ?? null;
441
532
  const isCurrent = n.id === monitorData.currentNodeId && monitorData.status === 'running';
442
- const status = exec?.status || (isCurrent ? 'running' : 'pending');
533
+ const status = latestExec?.status || (isCurrent ? 'running' : 'pending');
534
+ const execCount = executions?.length ?? 0;
443
535
  return {
444
536
  ...n,
445
537
  data: {
446
538
  ...n.data,
447
539
  _execStatus: status,
448
- _error: exec?.error || null,
540
+ _error: latestExec?.error || null,
541
+ _execCount: execCount,
449
542
  _onNodeClick: onNodeClick,
450
543
  },
451
544
  };
@@ -568,7 +661,7 @@ export function WorkflowMonitorModal({ featureId, featureName, isOpen, onClose }
568
661
  if (!isOpen) return null;
569
662
 
570
663
  const selectedNode = monitorData?.graphData.nodes.find(n => n.id === selectedNodeId);
571
- const selectedExec = selectedNodeId ? monitorData?.nodeExecutions[selectedNodeId] || null : null;
664
+ const selectedExecs = selectedNodeId ? monitorData?.nodeExecutions[selectedNodeId] || [] : [];
572
665
 
573
666
  return (
574
667
  <div className="fixed inset-0 z-[300]">
@@ -624,10 +717,11 @@ export function WorkflowMonitorModal({ featureId, featureName, isOpen, onClose }
624
717
  {/* Side panel */}
625
718
  {selectedNode && (
626
719
  <NodeDetailPanel
720
+ key={selectedNodeId}
627
721
  nodeId={selectedNodeId!}
628
722
  nodeType={selectedNode.type}
629
723
  nodeData={selectedNode.data}
630
- execution={selectedExec}
724
+ executions={selectedExecs}
631
725
  isCurrentNode={selectedNodeId === monitorData.currentNodeId}
632
726
  featureId={featureId}
633
727
  onClose={() => setSelectedNodeId(null)}
@@ -9,7 +9,7 @@ import { Handle, Position } from '@xyflow/react';
9
9
  import type { NodeProps } from '@xyflow/react';
10
10
  import {
11
11
  Play, Square, ArrowLeftRight, Bot, MapPin, RefreshCw, Tag, Layers,
12
- CircleDot, CheckCircle2, AlertTriangle, Clock, Package, Volume2, Film,
12
+ CircleDot, CheckCircle2, AlertTriangle, Clock, Package, Volume2, Film, Code,
13
13
  } from 'lucide-react';
14
14
 
15
15
  /* ── Execution status styling ── */
@@ -18,12 +18,14 @@ const STATUS_RING: Record<string, string> = {
18
18
  running: 'ring-2 ring-accent animate-pulse',
19
19
  completed: 'ring-2 ring-emerald-400',
20
20
  failed: 'ring-2 ring-error',
21
+ stopped: 'ring-2 ring-error',
21
22
  };
22
23
 
23
24
  const STATUS_BG: Record<string, string> = {
24
25
  running: 'bg-accent/10',
25
26
  completed: 'bg-emerald-500/10',
26
27
  failed: 'bg-error/10',
28
+ stopped: 'bg-error/10',
27
29
  pending: 'bg-white/[0.03]',
28
30
  };
29
31
 
@@ -31,6 +33,7 @@ function statusIcon(status: string, size = 14) {
31
33
  if (status === 'running') return <CircleDot size={size} className="text-accent animate-pulse" />;
32
34
  if (status === 'completed') return <CheckCircle2 size={size} className="text-emerald-400" />;
33
35
  if (status === 'failed') return <AlertTriangle size={size} className="text-error" />;
36
+ if (status === 'stopped') return <Square size={size} className="text-error" />;
34
37
  return <Clock size={size} className="text-content-muted" />;
35
38
  }
36
39
 
@@ -57,15 +60,22 @@ function MonitorNodeWrapper({
57
60
  }) {
58
61
  const execStatus = (data._execStatus as string) || 'pending';
59
62
  const error = data._error as string | null;
63
+ const execCount = (data._execCount as number) || 0;
60
64
  const onNodeClick = data._onNodeClick as ((nodeId: string) => void) | undefined;
61
65
  const ringCls = STATUS_RING[execStatus] || '';
62
66
  const bgCls = STATUS_BG[execStatus] || STATUS_BG.pending;
63
67
 
64
68
  return (
65
69
  <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`}
70
+ 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 relative`}
67
71
  onClick={(e) => { e.stopPropagation(); onNodeClick?.(id); }}
68
72
  >
73
+ {/* Execution count badge — shown when node has been executed more than once */}
74
+ {execCount > 1 && (
75
+ <div className="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-accent text-surface text-[10px] font-bold flex items-center justify-center shadow-sm">
76
+ {execCount}
77
+ </div>
78
+ )}
69
79
  {hasTarget && (
70
80
  <Handle type="target" position={Position.Top} className="!w-3 !h-3 !bg-accent !border-2 !border-surface" />
71
81
  )}
@@ -244,3 +254,17 @@ export function MonitorRenderVideoNode({ id, data }: NodeProps) {
244
254
  />
245
255
  );
246
256
  }
257
+
258
+ export function MonitorProgrammableNode({ id, data }: NodeProps) {
259
+ const d = data as Record<string, unknown>;
260
+ return (
261
+ <MonitorNodeWrapper
262
+ id={id}
263
+ data={d}
264
+ icon={<Code size={14} className="text-emerald-400" />}
265
+ label="Programmable"
266
+ detail={(d.label as string) || ''}
267
+ sourceHandles={['success', 'error']}
268
+ />
269
+ );
270
+ }
@@ -12,6 +12,7 @@ export const WORKFLOW_NODE_TYPES = [
12
12
  'rebuildBundle',
13
13
  'generateTTS',
14
14
  'renderVideo',
15
+ 'programmable',
15
16
  ] as const;
16
17
 
17
18
  export type WorkflowNodeType = typeof WORKFLOW_NODE_TYPES[number];
@@ -89,6 +90,21 @@ export interface RenderVideoData {
89
90
  fileOutputPrefix: string;
90
91
  }
91
92
 
93
+ export interface ProgrammableNodeData {
94
+ /** User-visible name — used as key in context.nodeOutputs */
95
+ label: string;
96
+ /** User-written JavaScript code containing async function run(context) { ... } */
97
+ code: string;
98
+ /** Execution timeout in milliseconds (default: 30000) */
99
+ timeout: number;
100
+ /** V8 isolate memory limit in MB (default: 128) */
101
+ memory: number;
102
+ /** Max HTTP requests per execution (default: 20) */
103
+ maxRequests: number;
104
+ /** Max HTTP response body size in bytes (default: 5242880) */
105
+ maxResponseSize: number;
106
+ }
107
+
92
108
  export type WorkflowNodeData =
93
109
  | ({ type: 'start' } & StartNodeData)
94
110
  | ({ type: 'transitionCard' } & TransitionCardData)
@@ -100,7 +116,8 @@ export type WorkflowNodeData =
100
116
  | ({ type: 'group' } & GroupNodeData)
101
117
  | ({ type: 'rebuildBundle' } & RebuildBundleData)
102
118
  | ({ type: 'generateTTS' } & GenerateTTSData)
103
- | ({ type: 'renderVideo' } & RenderVideoData);
119
+ | ({ type: 'renderVideo' } & RenderVideoData)
120
+ | ({ type: 'programmable' } & ProgrammableNodeData);
104
121
 
105
122
  export interface NodePaletteItem {
106
123
  type: WorkflowNodeType;
@@ -119,8 +136,31 @@ export const NODE_PALETTE_ITEMS: NodePaletteItem[] = [
119
136
  { type: 'rebuildBundle', label: 'Rebuild Bundle', description: 'Build Remotion webpack bundle' },
120
137
  { type: 'generateTTS', label: 'Generate TTS', description: 'Generate text-to-speech audio' },
121
138
  { type: 'renderVideo', label: 'Render Video', description: 'Render composition to MP4' },
139
+ { type: 'programmable', label: 'Programmable', description: 'Run custom JavaScript code' },
122
140
  ];
123
141
 
142
+ const DEFAULT_PROGRAMMABLE_CODE = `async function run(context) {
143
+ // Destructure the execution context
144
+ const { featureId, projectId, cycle, nodeOutputs } = context;
145
+
146
+ // Access output from a previous node
147
+ const prevResult = nodeOutputs["Previous Node"];
148
+
149
+ try {
150
+ // Example: make an HTTP request using the SSRF-protected fetch bridge
151
+ const url = "https://api.example.com/data";
152
+ const response = await fetch(url);
153
+ const data = await response.json();
154
+
155
+ // Return a value to follow the success output handle
156
+ return { data };
157
+ } catch (error) {
158
+ // Throw to follow the error output handle
159
+ throw error;
160
+ }
161
+ }
162
+ `;
163
+
124
164
  export const NODE_DEFAULTS: Record<string, () => Record<string, unknown>> = {
125
165
  start: () => ({}),
126
166
  transitionCard: () => ({ sourceColumn: 'todo', targetColumn: 'in_progress' }),
@@ -133,4 +173,5 @@ export const NODE_DEFAULTS: Record<string, () => Record<string, unknown>> = {
133
173
  rebuildBundle: () => ({}),
134
174
  generateTTS: () => ({ scriptPath: '', force: false, voiceId: '' }),
135
175
  renderVideo: () => ({ compositionId: '', resolution: '1920x1080', aspectRatio: '', fileOutputPrefix: '' }),
176
+ programmable: () => ({ label: '', code: DEFAULT_PROGRAMMABLE_CODE, timeout: 30000, memory: 128, maxRequests: 20, maxResponseSize: 5242880 }),
136
177
  };
@@ -0,0 +1,11 @@
1
+ import { useEffect } from 'react';
2
+
3
+ /**
4
+ * Sets document.title to 'AK-{title}' when the component mounts
5
+ * and updates on title changes.
6
+ */
7
+ export function useDocumentTitle(title: string): void {
8
+ useEffect(() => {
9
+ document.title = `AK-${title}`;
10
+ }, [title]);
11
+ }
@@ -19,6 +19,7 @@ export function useProjects() {
19
19
  selectProject: store.selectProject,
20
20
  createProject: store.createProject,
21
21
  renameProject: store.renameProject,
22
+ updatePreviewCommand: store.updatePreviewCommand,
22
23
  archiveProject: store.archiveProject,
23
24
  refetchProjects: store.refetchProjects,
24
25
  };