@codemation/next-host 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +25 -0
  2. package/app/(shell)/credentials/page.tsx +5 -0
  3. package/app/(shell)/dashboard/page.tsx +14 -0
  4. package/app/(shell)/layout.tsx +11 -0
  5. package/app/(shell)/page.tsx +5 -0
  6. package/app/(shell)/users/page.tsx +5 -0
  7. package/app/(shell)/workflows/[workflowId]/page.tsx +19 -0
  8. package/app/(shell)/workflows/page.tsx +5 -0
  9. package/app/api/[[...path]]/route.ts +40 -0
  10. package/app/api/auth/[...nextauth]/route.ts +3 -0
  11. package/app/globals.css +997 -0
  12. package/app/invite/[token]/page.tsx +10 -0
  13. package/app/layout.tsx +65 -0
  14. package/app/login/layout.tsx +25 -0
  15. package/app/login/page.tsx +22 -0
  16. package/components.json +21 -0
  17. package/docs/FORMS.md +46 -0
  18. package/docs/TAILWIND_SHADCN_MIGRATION.md +89 -0
  19. package/eslint.config.mjs +56 -0
  20. package/middleware.ts +29 -0
  21. package/next-env.d.ts +6 -0
  22. package/next.config.ts +34 -0
  23. package/package.json +76 -0
  24. package/postcss.config.mjs +7 -0
  25. package/public/canvas-icons/builtin/openai.svg +5 -0
  26. package/src/api/CodemationApiClient.ts +107 -0
  27. package/src/api/CodemationApiHttpError.ts +17 -0
  28. package/src/auth/CodemationNextAuthConfigResolver.ts +14 -0
  29. package/src/auth/CodemationNextAuthOAuthProviderDescriptorMapper.ts +30 -0
  30. package/src/auth/CodemationNextAuthOAuthProviderSnapshotResolver.ts +17 -0
  31. package/src/auth/CodemationNextAuthProviderCatalog.ts +107 -0
  32. package/src/auth/codemationEdgeAuth.ts +25 -0
  33. package/src/auth/codemationNextAuth.ts +32 -0
  34. package/src/components/Codemation.tsx +6 -0
  35. package/src/components/CodemationDataTable.tsx +37 -0
  36. package/src/components/CodemationDialog.tsx +137 -0
  37. package/src/components/CodemationFormattedDateTime.tsx +46 -0
  38. package/src/components/GoogleColorGIcon.tsx +39 -0
  39. package/src/components/OauthProviderIcon.tsx +33 -0
  40. package/src/components/PasswordStrengthMeter.tsx +59 -0
  41. package/src/components/forms/index.ts +28 -0
  42. package/src/components/json/JsonMonacoEditor.tsx +75 -0
  43. package/src/components/oauthProviderIconData.ts +17 -0
  44. package/src/components/ui/alert.tsx +56 -0
  45. package/src/components/ui/badge.tsx +40 -0
  46. package/src/components/ui/button.tsx +64 -0
  47. package/src/components/ui/card.tsx +70 -0
  48. package/src/components/ui/collapsible.tsx +26 -0
  49. package/src/components/ui/dialog.tsx +137 -0
  50. package/src/components/ui/dropdown-menu.tsx +238 -0
  51. package/src/components/ui/form.tsx +147 -0
  52. package/src/components/ui/input.tsx +19 -0
  53. package/src/components/ui/label.tsx +26 -0
  54. package/src/components/ui/scroll-area.tsx +47 -0
  55. package/src/components/ui/select.tsx +169 -0
  56. package/src/components/ui/separator.tsx +28 -0
  57. package/src/components/ui/switch.tsx +28 -0
  58. package/src/components/ui/table.tsx +72 -0
  59. package/src/components/ui/tabs.tsx +76 -0
  60. package/src/components/ui/textarea.tsx +18 -0
  61. package/src/components/ui/toggle.tsx +41 -0
  62. package/src/features/credentials/components/CredentialConfirmDialog.tsx +58 -0
  63. package/src/features/credentials/components/CredentialDialog.tsx +252 -0
  64. package/src/features/credentials/components/CredentialDialogFeedback.tsx +36 -0
  65. package/src/features/credentials/components/CredentialDialogFieldRows.tsx +257 -0
  66. package/src/features/credentials/components/CredentialDialogFormSections.tsx +230 -0
  67. package/src/features/credentials/components/CredentialEnvFieldStatusRow.tsx +64 -0
  68. package/src/features/credentials/components/CredentialFieldCopyButton.tsx +48 -0
  69. package/src/features/credentials/components/CredentialsScreenHealthBadge.tsx +21 -0
  70. package/src/features/credentials/components/CredentialsScreenInstancesTable.tsx +108 -0
  71. package/src/features/credentials/components/CredentialsScreenTestFailureAlert.tsx +33 -0
  72. package/src/features/credentials/hooks/useCredentialCreateDialog.ts +33 -0
  73. package/src/features/credentials/hooks/useCredentialDialogSession.ts +616 -0
  74. package/src/features/credentials/hooks/useCredentialsScreen.ts +213 -0
  75. package/src/features/credentials/lib/credentialFieldHelpers.ts +35 -0
  76. package/src/features/credentials/lib/credentialFormTypes.ts +1 -0
  77. package/src/features/credentials/lib/credentialInstanceTestPayloadParser.ts +10 -0
  78. package/src/features/credentials/screens/CredentialsScreen.tsx +187 -0
  79. package/src/features/invite/screens/InviteAcceptScreen.tsx +190 -0
  80. package/src/features/users/components/UsersInviteDialog.tsx +121 -0
  81. package/src/features/users/components/UsersRegenerateDialog.tsx +81 -0
  82. package/src/features/users/components/UsersScreenUserStatusBadge.tsx +19 -0
  83. package/src/features/users/schemas/usersInviteFormSchema.ts +7 -0
  84. package/src/features/users/screens/UsersScreen.tsx +240 -0
  85. package/src/features/workflows/components/WorkflowListFolderSection.tsx +91 -0
  86. package/src/features/workflows/components/WorkflowListItemCard.tsx +67 -0
  87. package/src/features/workflows/components/WorkflowListRoot.tsx +39 -0
  88. package/src/features/workflows/components/WorkflowsListTree.tsx +28 -0
  89. package/src/features/workflows/components/canvas/CanvasNodeChromeTooltip.tsx +96 -0
  90. package/src/features/workflows/components/canvas/CanvasNodeIconSlot.tsx +25 -0
  91. package/src/features/workflows/components/canvas/VisibleNodeStatusResolver.tsx +84 -0
  92. package/src/features/workflows/components/canvas/WorkflowCanvas.tsx +248 -0
  93. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNode.tsx +182 -0
  94. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAccents.tsx +73 -0
  95. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentBottomSourceHandles.tsx +43 -0
  96. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentLabels.tsx +47 -0
  97. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeCard.tsx +202 -0
  98. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeHandles.tsx +77 -0
  99. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeLabelBelow.tsx +51 -0
  100. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeMainGlyph.tsx +64 -0
  101. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeToolbar.tsx +95 -0
  102. package/src/features/workflows/components/canvas/WorkflowCanvasLoadingPlaceholder.tsx +69 -0
  103. package/src/features/workflows/components/canvas/WorkflowCanvasNodeIcon.tsx +102 -0
  104. package/src/features/workflows/components/canvas/WorkflowCanvasSimpleIconGlyph.tsx +21 -0
  105. package/src/features/workflows/components/canvas/WorkflowCanvasStraightCountEdge.tsx +33 -0
  106. package/src/features/workflows/components/canvas/WorkflowCanvasStructureSignature.tsx +7 -0
  107. package/src/features/workflows/components/canvas/WorkflowCanvasSymmetricForkEdge.tsx +32 -0
  108. package/src/features/workflows/components/canvas/WorkflowCanvasToolbarIconButton.tsx +95 -0
  109. package/src/features/workflows/components/canvas/lib/WorkflowCanvasBuiltinIconRegistry.ts +26 -0
  110. package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeCountResolver.ts +51 -0
  111. package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeStyleResolver.ts +35 -0
  112. package/src/features/workflows/components/canvas/lib/WorkflowCanvasLabelLayoutEstimator.ts +42 -0
  113. package/src/features/workflows/components/canvas/lib/WorkflowCanvasOverlapResolver.ts +78 -0
  114. package/src/features/workflows/components/canvas/lib/WorkflowCanvasPortOrderResolver.ts +25 -0
  115. package/src/features/workflows/components/canvas/lib/WorkflowCanvasRoundedOrthogonalPathPlanner.ts +56 -0
  116. package/src/features/workflows/components/canvas/lib/WorkflowCanvasSiIconRegistry.ts +18 -0
  117. package/src/features/workflows/components/canvas/lib/WorkflowCanvasSymmetricForkPathPlanner.ts +43 -0
  118. package/src/features/workflows/components/canvas/lib/layoutWorkflow.ts +315 -0
  119. package/src/features/workflows/components/canvas/lib/workflowCanvasEdgeGeometry.ts +3 -0
  120. package/src/features/workflows/components/canvas/lib/workflowCanvasEmbeddedStyles.ts +62 -0
  121. package/src/features/workflows/components/canvas/lib/workflowCanvasFlowTypes.ts +10 -0
  122. package/src/features/workflows/components/canvas/lib/workflowCanvasNodeData.ts +41 -0
  123. package/src/features/workflows/components/canvas/lib/workflowCanvasNodeGeometry.ts +99 -0
  124. package/src/features/workflows/components/canvas/workflowCanvasNodeChrome.tsx +46 -0
  125. package/src/features/workflows/components/realtime/RealtimeContext.tsx +14 -0
  126. package/src/features/workflows/components/realtime/WorkflowRealtimeProvider.tsx +15 -0
  127. package/src/features/workflows/components/workflowDetail/NodeCredentialBindingRow.tsx +209 -0
  128. package/src/features/workflows/components/workflowDetail/NodeCredentialBindingsSection.tsx +227 -0
  129. package/src/features/workflows/components/workflowDetail/NodePropertiesConfigSection.tsx +51 -0
  130. package/src/features/workflows/components/workflowDetail/NodePropertiesPanelHeader.tsx +50 -0
  131. package/src/features/workflows/components/workflowDetail/NodePropertiesSlidePanel.tsx +134 -0
  132. package/src/features/workflows/components/workflowDetail/WorkflowActivationErrorDialog.tsx +71 -0
  133. package/src/features/workflows/components/workflowDetail/WorkflowActivationHeaderControl.tsx +64 -0
  134. package/src/features/workflows/components/workflowDetail/WorkflowDetailIcons.tsx +52 -0
  135. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspector.tsx +110 -0
  136. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorDetailBody.tsx +213 -0
  137. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorPanes.tsx +239 -0
  138. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorSidebarResizer.tsx +31 -0
  139. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorTreePanel.tsx +133 -0
  140. package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentGroupingPresenter.tsx +31 -0
  141. package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentList.tsx +118 -0
  142. package/src/features/workflows/components/workflowDetail/WorkflowInspectorBinaryView.tsx +15 -0
  143. package/src/features/workflows/components/workflowDetail/WorkflowInspectorErrorView.tsx +107 -0
  144. package/src/features/workflows/components/workflowDetail/WorkflowInspectorJsonView.tsx +114 -0
  145. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreePresenter.tsx +132 -0
  146. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreeViewRenderer.tsx +147 -0
  147. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyView.tsx +65 -0
  148. package/src/features/workflows/components/workflowDetail/WorkflowInspectorViews.tsx +5 -0
  149. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryAttachmentRow.tsx +74 -0
  150. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryUploadRow.tsx +69 -0
  151. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorDialog.tsx +254 -0
  152. package/src/features/workflows/components/workflowDetail/WorkflowRunsList.tsx +89 -0
  153. package/src/features/workflows/components/workflowDetail/WorkflowRunsSidebar.tsx +50 -0
  154. package/src/features/workflows/hooks/canvas/useWorkflowCanvasVisibleNodeStatuses.ts +14 -0
  155. package/src/features/workflows/hooks/realtime/realtime.tsx +271 -0
  156. package/src/features/workflows/hooks/realtime/runQueryPolling.ts +34 -0
  157. package/src/features/workflows/hooks/realtime/useWorkflowRealtimeInfrastructure.ts +541 -0
  158. package/src/features/workflows/hooks/realtime/useWorkflowRealtimeShowDisconnectedBadge.ts +9 -0
  159. package/src/features/workflows/hooks/workflowDetail/useWorkflowDetailController.tsx +1300 -0
  160. package/src/features/workflows/lib/realtime/realtimeApi.ts +78 -0
  161. package/src/features/workflows/lib/realtime/realtimeClientBridge.ts +52 -0
  162. package/src/features/workflows/lib/realtime/realtimeDomainTypes.ts +191 -0
  163. package/src/features/workflows/lib/realtime/realtimeQueryKeys.ts +15 -0
  164. package/src/features/workflows/lib/realtime/realtimeRunMutations.ts +167 -0
  165. package/src/features/workflows/lib/realtime/workflowTypes.ts +5 -0
  166. package/src/features/workflows/lib/workflowDetail/PersistedWorkflowSnapshotMapper.ts +205 -0
  167. package/src/features/workflows/lib/workflowDetail/WorkflowActivationHttpErrorFormat.ts +32 -0
  168. package/src/features/workflows/lib/workflowDetail/WorkflowDetailPresenter.ts +1017 -0
  169. package/src/features/workflows/lib/workflowDetail/WorkflowDetailUrlCodec.ts +70 -0
  170. package/src/features/workflows/lib/workflowDetail/workflowDetailTypes.ts +152 -0
  171. package/src/features/workflows/lib/workflowDetailTreeStyles.ts +65 -0
  172. package/src/features/workflows/screens/WorkflowDetailScreen.tsx +236 -0
  173. package/src/features/workflows/screens/WorkflowDetailScreenInspectorPanel.tsx +55 -0
  174. package/src/features/workflows/screens/WorkflowsList.tsx +35 -0
  175. package/src/features/workflows/screens/WorkflowsScreen.tsx +31 -0
  176. package/src/index.ts +1 -0
  177. package/src/lib/utils.ts +6 -0
  178. package/src/middleware/CodemationNextHostMiddlewarePathRules.ts +31 -0
  179. package/src/providers/CodemationSessionProvider.tsx +23 -0
  180. package/src/providers/Providers.tsx +36 -0
  181. package/src/providers/RealtimeBoundary.tsx +17 -0
  182. package/src/providers/WhitelabelProvider.tsx +22 -0
  183. package/src/server/CodemationAuthPrismaClient.ts +21 -0
  184. package/src/server/CodemationNextHost.ts +379 -0
  185. package/src/shell/AppLayout.tsx +141 -0
  186. package/src/shell/AppLayoutNavItems.tsx +129 -0
  187. package/src/shell/AppLayoutPageHeader.tsx +79 -0
  188. package/src/shell/AppLayoutSidebarBrand.tsx +33 -0
  189. package/src/shell/AppMainContent.tsx +17 -0
  190. package/src/shell/AppShellHeaderActions.tsx +12 -0
  191. package/src/shell/AppShellHeaderActionsAuthenticated.tsx +51 -0
  192. package/src/shell/CodemationNextClientShell.tsx +17 -0
  193. package/src/shell/CredentialsSignInRedirectResolver.ts +21 -0
  194. package/src/shell/LoginPageClient.tsx +231 -0
  195. package/src/shell/WorkflowDetailChromeContext.tsx +42 -0
  196. package/src/shell/WorkflowFolderTreeBuilder.ts +62 -0
  197. package/src/shell/WorkflowFolderUi.ts +42 -0
  198. package/src/shell/WorkflowSidebarNavFolder.tsx +112 -0
  199. package/src/shell/WorkflowSidebarNavTree.tsx +68 -0
  200. package/src/shell/appLayoutPageTitle.ts +16 -0
  201. package/src/shell/appLayoutSidebarIcons.tsx +108 -0
  202. package/src/whitelabel/CodemationWhitelabelSnapshot.ts +4 -0
  203. package/src/whitelabel/CodemationWhitelabelSnapshotFactory.ts +18 -0
  204. package/tsconfig.json +40 -0
  205. package/tsconfig.tsbuildinfo +1 -0
  206. package/vitest.config.ts +34 -0
@@ -0,0 +1,18 @@
1
+ import type { SimpleIcon } from "simple-icons";
2
+ import { siGmail } from "simple-icons";
3
+
4
+ /**
5
+ * Cherry-picked Simple Icons for canvas use. Add named imports here when authors use `si:<slug>`.
6
+ * Prefer **builtin** assets ({@link WorkflowCanvasBuiltinIconRegistry}) for brands with official marks;
7
+ * use `si:` only when the icon is in `simple-icons` and not duplicated as a builtin.
8
+ * Authors may also set `icon` to an image URL for any brand not listed.
9
+ */
10
+ export class WorkflowCanvasSiIconRegistry {
11
+ private static readonly slugToIcon: ReadonlyMap<string, SimpleIcon> = new Map<string, SimpleIcon>([
12
+ ["gmail", siGmail],
13
+ ]);
14
+
15
+ static resolve(slug: string): SimpleIcon | undefined {
16
+ return this.slugToIcon.get(slug);
17
+ }
18
+ }
@@ -0,0 +1,43 @@
1
+ import { WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS } from "./workflowCanvasEdgeGeometry";
2
+ import { WorkflowCanvasRoundedOrthogonalPathPlanner } from "./WorkflowCanvasRoundedOrthogonalPathPlanner";
3
+
4
+ /**
5
+ * Orthogonal path for fork edges (one source → many targets) so every branch
6
+ * shares the same first horizontal segment, then drops or rises vertically—mirroring
7
+ * how merge edges converge symmetrically on the right. Corners use the same
8
+ * quadratic smoothing as React Flow smoothstep edges.
9
+ */
10
+ export class WorkflowCanvasSymmetricForkPathPlanner {
11
+ static build(
12
+ args: Readonly<{
13
+ sourceX: number;
14
+ sourceY: number;
15
+ targetX: number;
16
+ targetY: number;
17
+ offset: number;
18
+ }>,
19
+ ): Readonly<{ path: string; labelX: number; labelY: number }> {
20
+ const { sourceX, sourceY, targetX, targetY, offset } = args;
21
+ let x1 = sourceX + offset;
22
+ let x2 = targetX - offset;
23
+ if (x1 >= x2 - 0.5) {
24
+ const mid = (sourceX + targetX) / 2;
25
+ x1 = mid;
26
+ x2 = mid;
27
+ }
28
+ const points = [
29
+ { x: sourceX, y: sourceY },
30
+ { x: x1, y: sourceY },
31
+ { x: x1, y: targetY },
32
+ { x: x2, y: targetY },
33
+ { x: targetX, y: targetY },
34
+ ];
35
+ const path = WorkflowCanvasRoundedOrthogonalPathPlanner.buildPathFromPoints(
36
+ points,
37
+ WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS,
38
+ );
39
+ const labelX = (x1 + x2) / 2;
40
+ const labelY = targetY;
41
+ return { path, labelX, labelY };
42
+ }
43
+ }
@@ -0,0 +1,315 @@
1
+ import dagre from "dagre";
2
+ import { MarkerType, Position, type Edge as ReactFlowEdge, type Node as ReactFlowNode } from "@xyflow/react";
3
+
4
+ import type { ConnectionInvocationRecord, NodeExecutionSnapshot } from "../../../lib/realtime/realtimeDomainTypes";
5
+ import type { WorkflowDto } from "../../../lib/realtime/workflowTypes";
6
+ import type { WorkflowCanvasNodeData } from "./workflowCanvasNodeData";
7
+ import { WorkflowCanvasEdgeCountResolver } from "./WorkflowCanvasEdgeCountResolver";
8
+ import { WorkflowCanvasEdgeStyleResolver } from "./WorkflowCanvasEdgeStyleResolver";
9
+ import {
10
+ WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS,
11
+ WORKFLOW_CANVAS_MAIN_EDGE_OFFSET,
12
+ } from "./workflowCanvasEdgeGeometry";
13
+ import {
14
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX,
15
+ WORKFLOW_CANVAS_MAIN_NODE_CARD_PX,
16
+ WorkflowCanvasNodeGeometry,
17
+ } from "./workflowCanvasNodeGeometry";
18
+
19
+ function mainWorkflowNodeWidthPx(role: string | undefined): number {
20
+ return WorkflowCanvasNodeGeometry.mainNodeWidthPx(role === "agent");
21
+ }
22
+ import { WorkflowCanvasOverlapResolver } from "./WorkflowCanvasOverlapResolver";
23
+ import { WorkflowCanvasPortOrderResolver } from "./WorkflowCanvasPortOrderResolver";
24
+
25
+ export function layoutWorkflow(
26
+ workflow: WorkflowDto,
27
+ nodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>,
28
+ connectionInvocations: ReadonlyArray<ConnectionInvocationRecord>,
29
+ nodeStatusesByNodeId: Readonly<Record<string, NodeExecutionSnapshot["status"] | undefined>>,
30
+ credentialAttentionTooltipByNodeId: ReadonlyMap<string, string>,
31
+ selectedNodeId: string | null,
32
+ propertiesTargetNodeId: string | null,
33
+ pinnedNodeIds: ReadonlySet<string>,
34
+ isLiveWorkflowView: boolean,
35
+ isRunning: boolean,
36
+ workflowNodeIdsWithBoundCredential: ReadonlySet<string>,
37
+ onSelectNode: (nodeId: string) => void,
38
+ onOpenPropertiesNode: (nodeId: string) => void,
39
+ onRequestOpenCredentialEditForNode: (nodeId: string) => void,
40
+ onRunNode: (nodeId: string) => void,
41
+ onTogglePinnedOutput: (nodeId: string) => void,
42
+ onEditNodeOutput: (nodeId: string) => void,
43
+ onClearPinnedOutput: (nodeId: string) => void,
44
+ ): Readonly<{ nodes: ReactFlowNode<WorkflowCanvasNodeData>[]; edges: ReactFlowEdge[] }> {
45
+ const dagreGraph = new dagre.graphlib.Graph();
46
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
47
+ dagreGraph.setGraph({ rankdir: "LR", ranksep: 128, nodesep: 56, edgesep: 20 });
48
+
49
+ const attachmentNodeWidth = WorkflowCanvasNodeGeometry.attachmentNodeWidthPx();
50
+ const attachmentXSpacing = attachmentNodeWidth + 36;
51
+ const layoutNodes = workflow.nodes.filter((node) => !node.parentNodeId);
52
+ const layoutNodeIds = new Set(layoutNodes.map((node) => node.id));
53
+ const layoutEdges = workflow.edges.filter(
54
+ (edge) => layoutNodeIds.has(edge.from.nodeId) && layoutNodeIds.has(edge.to.nodeId),
55
+ );
56
+
57
+ for (const node of layoutNodes) {
58
+ dagreGraph.setNode(node.id, {
59
+ width: mainWorkflowNodeWidthPx(node.role),
60
+ height: WORKFLOW_CANVAS_MAIN_NODE_CARD_PX,
61
+ });
62
+ }
63
+ for (const [i, edge] of layoutEdges.entries()) {
64
+ dagreGraph.setEdge(edge.from.nodeId, edge.to.nodeId, { i });
65
+ }
66
+
67
+ dagre.layout(dagreGraph);
68
+
69
+ const positionsByNodeId = new Map<string, { x: number; y: number }>();
70
+ for (const node of layoutNodes) {
71
+ const position = dagreGraph.node(node.id) as { x: number; y: number } | undefined;
72
+ positionsByNodeId.set(node.id, { x: position?.x ?? 0, y: position?.y ?? 0 });
73
+ }
74
+
75
+ const attachmentNodesByParentNodeId = new Map<string, WorkflowDto["nodes"]>();
76
+ for (const node of workflow.nodes) {
77
+ if (!node.parentNodeId) continue;
78
+ const siblings = attachmentNodesByParentNodeId.get(node.parentNodeId) ?? [];
79
+ attachmentNodesByParentNodeId.set(node.parentNodeId, [...siblings, node]);
80
+ }
81
+
82
+ for (const [parentNodeId, attachmentNodes] of attachmentNodesByParentNodeId.entries()) {
83
+ const parentPosition = positionsByNodeId.get(parentNodeId);
84
+ if (!parentPosition) continue;
85
+ const parentMeta = layoutNodes.find((n) => n.id === parentNodeId);
86
+ const parentLabel = parentMeta?.name ?? parentMeta?.type ?? parentNodeId;
87
+ const parentIsAgent = parentMeta?.role === "agent";
88
+ if (attachmentNodes.length === 0) {
89
+ continue;
90
+ }
91
+ const attachmentYOffset = WorkflowCanvasNodeGeometry.attachmentCardCenterYDeltaFromParentCardCenter(
92
+ parentLabel,
93
+ parentIsAgent,
94
+ );
95
+ const orderedAttachmentNodes = [...attachmentNodes].sort((left, right) => {
96
+ if (left.role === right.role) return left.name?.localeCompare(right.name ?? "") ?? 0;
97
+ if (left.role === "languageModel") return -1;
98
+ if (right.role === "languageModel") return 1;
99
+ return 0;
100
+ });
101
+ orderedAttachmentNodes.forEach((attachmentNode, index) => {
102
+ positionsByNodeId.set(attachmentNode.id, {
103
+ x: parentPosition.x + (index - (attachmentNodes.length - 1) / 2) * attachmentXSpacing,
104
+ y: parentPosition.y + attachmentYOffset,
105
+ });
106
+ });
107
+ }
108
+
109
+ const widthByNodeId = new Map<string, number>();
110
+ const heightByNodeId = new Map<string, number>();
111
+ for (const n of workflow.nodes) {
112
+ widthByNodeId.set(n.id, n.parentNodeId ? attachmentNodeWidth : mainWorkflowNodeWidthPx(n.role));
113
+ heightByNodeId.set(
114
+ n.id,
115
+ n.parentNodeId ? WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX : WORKFLOW_CANVAS_MAIN_NODE_CARD_PX,
116
+ );
117
+ }
118
+ const resolvedPositions = WorkflowCanvasOverlapResolver.resolve({
119
+ positionsByNodeId,
120
+ widthByNodeId,
121
+ heightByNodeId,
122
+ gap: 10,
123
+ });
124
+ for (const [id, pos] of resolvedPositions) {
125
+ positionsByNodeId.set(id, pos);
126
+ }
127
+
128
+ const outgoingOutputsByNodeId = new Map<string, Set<string>>();
129
+ const incomingInputsByNodeId = new Map<string, Set<string>>();
130
+ for (const edge of workflow.edges) {
131
+ if (!outgoingOutputsByNodeId.has(edge.from.nodeId)) {
132
+ outgoingOutputsByNodeId.set(edge.from.nodeId, new Set());
133
+ }
134
+ outgoingOutputsByNodeId.get(edge.from.nodeId)!.add(edge.from.output);
135
+ if (!incomingInputsByNodeId.has(edge.to.nodeId)) {
136
+ incomingInputsByNodeId.set(edge.to.nodeId, new Set());
137
+ }
138
+ incomingInputsByNodeId.get(edge.to.nodeId)!.add(edge.to.input);
139
+ }
140
+
141
+ const nodes: ReactFlowNode<WorkflowCanvasNodeData>[] = workflow.nodes.map((n) => {
142
+ const pos = positionsByNodeId.get(n.id);
143
+ const label = n.name ?? n.type ?? n.id;
144
+ const resolvedNodeWidth = n.parentNodeId ? attachmentNodeWidth : mainWorkflowNodeWidthPx(n.role);
145
+ const resolvedNodeHeight = n.parentNodeId
146
+ ? WorkflowCanvasNodeGeometry.attachmentNodeHeightPx(label)
147
+ : WorkflowCanvasNodeGeometry.mainNodeHeightPx(label, n.role === "agent");
148
+ const layoutCardHeightPx = n.parentNodeId
149
+ ? WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX
150
+ : WORKFLOW_CANVAS_MAIN_NODE_CARD_PX;
151
+ const rawOut = outgoingOutputsByNodeId.get(n.id);
152
+ const rawIn = incomingInputsByNodeId.get(n.id);
153
+ const sourceOutputPorts = WorkflowCanvasPortOrderResolver.sortSourceOutputs(
154
+ rawOut && rawOut.size > 0 ? [...rawOut] : ["main"],
155
+ );
156
+ const targetInputPorts = WorkflowCanvasPortOrderResolver.sortTargetInputs(
157
+ rawIn && rawIn.size > 0 ? [...rawIn] : ["in"],
158
+ );
159
+ return {
160
+ id: n.id,
161
+ type: "codemation",
162
+ position: {
163
+ x: (pos?.x ?? 0) - resolvedNodeWidth / 2,
164
+ y: (pos?.y ?? 0) - layoutCardHeightPx / 2,
165
+ },
166
+ width: resolvedNodeWidth,
167
+ height: resolvedNodeHeight,
168
+ initialWidth: resolvedNodeWidth,
169
+ initialHeight: resolvedNodeHeight,
170
+ data: {
171
+ nodeId: n.id,
172
+ label,
173
+ type: n.type,
174
+ kind: n.kind,
175
+ role: n.role,
176
+ icon: n.icon,
177
+ status: nodeStatusesByNodeId[n.id],
178
+ selected: selectedNodeId === n.id,
179
+ propertiesTarget: propertiesTargetNodeId === n.id,
180
+ isAttachment: Boolean(n.parentNodeId),
181
+ isPinned: pinnedNodeIds.has(n.id),
182
+ hasOutputData: Boolean(pinnedNodeIds.has(n.id) || nodeSnapshotsByNodeId[n.id]?.outputs?.main),
183
+ isLiveWorkflowView,
184
+ isRunning,
185
+ retryPolicySummary: n.retryPolicySummary,
186
+ hasNodeErrorHandler: n.hasNodeErrorHandler,
187
+ continueWhenEmptyOutput: n.continueWhenEmptyOutput,
188
+ credentialAttentionTooltip: credentialAttentionTooltipByNodeId.get(n.id),
189
+ sourceOutputPorts,
190
+ targetInputPorts,
191
+ onSelectNode,
192
+ onOpenPropertiesNode,
193
+ onRunNode,
194
+ onTogglePinnedOutput,
195
+ onEditNodeOutput,
196
+ onClearPinnedOutput,
197
+ showCredentialEditToolbar: isLiveWorkflowView && workflowNodeIdsWithBoundCredential.has(n.id),
198
+ onOpenCredentialEditFromCanvas:
199
+ isLiveWorkflowView && workflowNodeIdsWithBoundCredential.has(n.id)
200
+ ? () => onRequestOpenCredentialEditForNode(n.id)
201
+ : undefined,
202
+ layoutWidthPx: resolvedNodeWidth,
203
+ layoutHeightPx: resolvedNodeHeight,
204
+ },
205
+ draggable: false,
206
+ sourcePosition: n.parentNodeId ? Position.Bottom : Position.Right,
207
+ targetPosition: n.parentNodeId ? Position.Top : Position.Left,
208
+ };
209
+ });
210
+
211
+ const nodesById = new Map(workflow.nodes.map((node) => [node.id, node]));
212
+ const edges: ReactFlowEdge[] = workflow.edges.map((e, i) => {
213
+ const targetNode = nodesById.get(e.to.nodeId);
214
+ const isAttachmentEdge = targetNode?.role === "languageModel" || targetNode?.role === "tool";
215
+ const attachmentSourceHandle =
216
+ targetNode?.role === "languageModel"
217
+ ? "attachment-llm-source"
218
+ : targetNode?.role === "tool"
219
+ ? "attachment-tools-source"
220
+ : undefined;
221
+ const outgoingFromSourceCount = outgoingOutputsByNodeId.get(e.from.nodeId)?.size ?? 0;
222
+ const incomingToTargetCount = incomingInputsByNodeId.get(e.to.nodeId)?.size ?? 0;
223
+ const useSharedBranchSourceHandle = !isAttachmentEdge && outgoingFromSourceCount > 1;
224
+ const useSharedBranchTargetHandle = !isAttachmentEdge && incomingToTargetCount > 1;
225
+ const sourcePosition = positionsByNodeId.get(e.from.nodeId);
226
+ const targetPosition = positionsByNodeId.get(e.to.nodeId);
227
+ const isStraightMainEdge = !isAttachmentEdge && Math.abs((sourcePosition?.y ?? 0) - (targetPosition?.y ?? 0)) < 1;
228
+ const targetSnapshot = nodeSnapshotsByNodeId[e.to.nodeId];
229
+ const sourceSnapshot = nodeSnapshotsByNodeId[e.from.nodeId];
230
+ const edgeItemCount = WorkflowCanvasEdgeCountResolver.resolveCount({
231
+ targetNodeId: e.to.nodeId,
232
+ targetNodeRole: targetNode?.role,
233
+ targetInput: e.to.input,
234
+ sourceOutput: e.from.output,
235
+ sourceSnapshot,
236
+ targetSnapshot,
237
+ nodeSnapshotsByNodeId,
238
+ connectionInvocations,
239
+ });
240
+ const edgeLabel = edgeItemCount > 0 ? `${edgeItemCount} item${edgeItemCount === 1 ? "" : "s"}` : undefined;
241
+ const edgeStroke = WorkflowCanvasEdgeStyleResolver.resolveStrokeColor({
242
+ edgeItemCount,
243
+ isAttachmentEdge,
244
+ });
245
+ const mainSourceHandle = isAttachmentEdge
246
+ ? attachmentSourceHandle
247
+ : useSharedBranchSourceHandle
248
+ ? undefined
249
+ : e.from.output;
250
+ const mainTargetHandle = isAttachmentEdge
251
+ ? "attachment-target"
252
+ : useSharedBranchTargetHandle
253
+ ? undefined
254
+ : e.to.input;
255
+ const isForkOutgoingEdge = !isAttachmentEdge && !isStraightMainEdge && outgoingFromSourceCount > 1;
256
+ return {
257
+ id: `${e.from.nodeId}:${e.from.output}->${e.to.nodeId}:${e.to.input}:${i}`,
258
+ source: e.from.nodeId,
259
+ target: e.to.nodeId,
260
+ sourceHandle: mainSourceHandle,
261
+ targetHandle: mainTargetHandle,
262
+ animated: false,
263
+ type: isAttachmentEdge
264
+ ? "smoothstep"
265
+ : isStraightMainEdge
266
+ ? "straightCount"
267
+ : isForkOutgoingEdge
268
+ ? "symmetricFork"
269
+ : "smoothstep",
270
+ pathOptions:
271
+ isAttachmentEdge || isStraightMainEdge || isForkOutgoingEdge
272
+ ? undefined
273
+ : {
274
+ borderRadius: WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS,
275
+ offset: WORKFLOW_CANVAS_MAIN_EDGE_OFFSET,
276
+ stepPosition: 0.5,
277
+ },
278
+ style: {
279
+ stroke: edgeStroke,
280
+ strokeWidth: isAttachmentEdge ? 1.35 : 1.5,
281
+ strokeDasharray: isAttachmentEdge ? "2 6" : undefined,
282
+ strokeLinecap: isAttachmentEdge ? "round" : "round",
283
+ strokeLinejoin: isAttachmentEdge ? undefined : "round",
284
+ },
285
+ label: edgeLabel,
286
+ labelStyle: {
287
+ fill: WorkflowCanvasEdgeStyleResolver.resolveLabelFill({
288
+ edgeItemCount,
289
+ isAttachmentEdge,
290
+ }),
291
+ fontSize: isAttachmentEdge ? 10 : 11,
292
+ fontWeight: 800,
293
+ },
294
+ labelBgStyle: {
295
+ fill: WorkflowCanvasEdgeStyleResolver.resolveLabelBackground({
296
+ edgeItemCount,
297
+ isAttachmentEdge,
298
+ }),
299
+ fillOpacity: 1,
300
+ },
301
+ labelBgPadding: isAttachmentEdge ? [4, 2] : [6, 3],
302
+ labelBgBorderRadius: 4,
303
+ markerEnd: isAttachmentEdge
304
+ ? undefined
305
+ : {
306
+ type: MarkerType.ArrowClosed,
307
+ width: 18,
308
+ height: 18,
309
+ color: edgeStroke,
310
+ },
311
+ };
312
+ });
313
+
314
+ return { nodes, edges };
315
+ }
@@ -0,0 +1,3 @@
1
+ /** Matches `smoothstep` `pathOptions` on non-fork main edges (merge / chain). */
2
+ export const WORKFLOW_CANVAS_MAIN_EDGE_CORNER_RADIUS = 6;
3
+ export const WORKFLOW_CANVAS_MAIN_EDGE_OFFSET = 34;
@@ -0,0 +1,62 @@
1
+ export const WORKFLOW_CANVAS_EMBEDDED_STYLES = `
2
+ @property --codemation-node-ring-angle {
3
+ syntax: "<angle>";
4
+ initial-value: 0deg;
5
+ inherits: false;
6
+ }
7
+
8
+ @keyframes codemationNodeSpin {
9
+ from {
10
+ transform: rotate(0deg);
11
+ }
12
+ to {
13
+ transform: rotate(360deg);
14
+ }
15
+ }
16
+
17
+ @keyframes codemationNodeBreath {
18
+ 0%,
19
+ 100% {
20
+ opacity: 0.45;
21
+ transform: scale(0.992);
22
+ }
23
+ 45% {
24
+ opacity: 0.92;
25
+ transform: scale(1.018);
26
+ }
27
+ 70% {
28
+ opacity: 0.72;
29
+ transform: scale(1.003);
30
+ }
31
+ }
32
+
33
+ @keyframes codemationNodeRingRotate {
34
+ from {
35
+ --codemation-node-ring-angle: 0deg;
36
+ }
37
+ to {
38
+ --codemation-node-ring-angle: 360deg;
39
+ }
40
+ }
41
+
42
+ @keyframes codemationCanvasLoaderPulse {
43
+ 0%,
44
+ 100% {
45
+ opacity: 0.45;
46
+ transform: scale(0.9);
47
+ }
48
+ 50% {
49
+ opacity: 1;
50
+ transform: scale(1);
51
+ }
52
+ }
53
+
54
+ @keyframes codemationCanvasLoaderShimmer {
55
+ 0% {
56
+ background-position: 200% 0;
57
+ }
58
+ 100% {
59
+ background-position: -200% 0;
60
+ }
61
+ }
62
+ `;
@@ -0,0 +1,10 @@
1
+ import { CodemationNode } from "../WorkflowCanvasCodemationNode";
2
+ import { StraightCountEdge } from "../WorkflowCanvasStraightCountEdge";
3
+ import { WorkflowCanvasSymmetricForkEdge } from "../WorkflowCanvasSymmetricForkEdge";
4
+
5
+ export const workflowCanvasNodeTypes = { codemation: CodemationNode };
6
+
7
+ export const workflowCanvasEdgeTypes = {
8
+ straightCount: StraightCountEdge,
9
+ symmetricFork: WorkflowCanvasSymmetricForkEdge,
10
+ };
@@ -0,0 +1,41 @@
1
+ import type { NodeExecutionSnapshot } from "../../../lib/realtime/realtimeDomainTypes";
2
+
3
+ export type WorkflowCanvasNodeData = Readonly<{
4
+ nodeId: string;
5
+ label: string;
6
+ type: string;
7
+ kind: string;
8
+ role?: string;
9
+ icon?: string;
10
+ status?: NodeExecutionSnapshot["status"];
11
+ selected: boolean;
12
+ propertiesTarget: boolean;
13
+ isAttachment: boolean;
14
+ isPinned: boolean;
15
+ hasOutputData: boolean;
16
+ isLiveWorkflowView: boolean;
17
+ isRunning: boolean;
18
+ retryPolicySummary?: string;
19
+ hasNodeErrorHandler?: boolean;
20
+ /** When true, empty main batches still schedule downstream; surfaced on the canvas. */
21
+ continueWhenEmptyOutput?: boolean;
22
+ /** When set, show a credential warning icon with this tooltip (required slot unbound). */
23
+ credentialAttentionTooltip?: string;
24
+ /** Distinct source output port names on this node (for multi-handle Right routing). */
25
+ sourceOutputPorts: readonly string[];
26
+ /** Distinct target input port names on this node (for multi-handle Left routing). */
27
+ targetInputPorts: readonly string[];
28
+ /** Matches Dagre / React Flow measured bounds (label wrap + agent badge row). */
29
+ layoutWidthPx: number;
30
+ layoutHeightPx: number;
31
+ onSelectNode: (nodeId: string) => void;
32
+ onOpenPropertiesNode: (nodeId: string) => void;
33
+ onRunNode: (nodeId: string) => void;
34
+ onTogglePinnedOutput: (nodeId: string) => void;
35
+ onEditNodeOutput: (nodeId: string) => void;
36
+ onClearPinnedOutput: (nodeId: string) => void;
37
+ /** Live workflow: node has a bound credential — toolbar can open the edit dialog. */
38
+ showCredentialEditToolbar?: boolean;
39
+ /** Opens properties (if needed) and the credential edit dialog for this node. */
40
+ onOpenCredentialEditFromCanvas?: () => void;
41
+ }>;
@@ -0,0 +1,99 @@
1
+ import { WorkflowCanvasLabelLayoutEstimator } from "./WorkflowCanvasLabelLayoutEstimator";
2
+
3
+ /** Main workflow node: square card + optional agent badge row + label strip below (React Flow node bounds). ~10% larger than initial canvas revision. */
4
+ export const WORKFLOW_CANVAS_MAIN_NODE_CARD_PX = 92;
5
+ /** AI Agent: wider card with the node title inline next to the icon (no separate label strip below the card). */
6
+ export const WORKFLOW_CANVAS_AGENT_NODE_CARD_WIDTH_PX = 220;
7
+ export const WORKFLOW_CANVAS_MAIN_NODE_LABEL_GAP_PX = 5;
8
+ export const WORKFLOW_CANVAS_MAIN_NODE_ICON_PX = 22;
9
+ /** Unified stroke for Lucide canvas glyphs (matches visual weight of filled brand marks). */
10
+ export const WORKFLOW_CANVAS_NODE_ICON_STROKE_WIDTH = 2;
11
+ export const WORKFLOW_CANVAS_MAIN_NODE_LABEL_FONT_PX = 13;
12
+ export const WORKFLOW_CANVAS_MAIN_NODE_LABEL_LINE_HEIGHT = 1.25;
13
+
14
+ /** Attachment nodes (LLM/tools): smaller square + label below. */
15
+ export const WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX = 74;
16
+ export const WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_GAP_PX = 5;
17
+ /** Proportional to main card icon (~22×74/92); keeps attachment row readable vs main row. */
18
+ export const WORKFLOW_CANVAS_ATTACHMENT_NODE_ICON_PX = 18;
19
+ export const WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_FONT_PX = 12;
20
+ export const WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_LINE_HEIGHT = 1.25;
21
+
22
+ /** Horizontal padding inside label (matches LabelBelow padding). */
23
+ export const WORKFLOW_CANVAS_LABEL_HORIZONTAL_PADDING_PX = 9;
24
+
25
+ /** In-flow row under agent card for LLM / Tools chips (see WorkflowCanvasCodemationNodeAgentLabels). */
26
+ export const WORKFLOW_CANVAS_AGENT_BADGE_ROW_PX = 24;
27
+
28
+ /** Vertical gap between main node bottom and attachment row center line (layout). Extra slack so edges clear agent labels/badges. */
29
+ export const WORKFLOW_CANVAS_ATTACHMENT_STACK_GAP_PX = 50;
30
+
31
+ export class WorkflowCanvasNodeGeometry {
32
+ static mainNodeWidthPx(isAgent: boolean): number {
33
+ return isAgent ? WORKFLOW_CANVAS_AGENT_NODE_CARD_WIDTH_PX : WORKFLOW_CANVAS_MAIN_NODE_CARD_PX;
34
+ }
35
+
36
+ static mainNodeLabelBlockHeightPx(label: string): number {
37
+ const maxW = Math.max(32, WORKFLOW_CANVAS_MAIN_NODE_CARD_PX - WORKFLOW_CANVAS_LABEL_HORIZONTAL_PADDING_PX);
38
+ const lines = WorkflowCanvasLabelLayoutEstimator.estimateLineCount(
39
+ label,
40
+ maxW,
41
+ WORKFLOW_CANVAS_MAIN_NODE_LABEL_FONT_PX,
42
+ );
43
+ const lineHeightPx = WORKFLOW_CANVAS_MAIN_NODE_LABEL_FONT_PX * WORKFLOW_CANVAS_MAIN_NODE_LABEL_LINE_HEIGHT;
44
+ return lines * lineHeightPx + 2;
45
+ }
46
+
47
+ static mainNodeHeightPx(label: string, isAgent: boolean): number {
48
+ const badge = isAgent ? WORKFLOW_CANVAS_AGENT_BADGE_ROW_PX : 0;
49
+ const labelBelowCard = isAgent ? 0 : this.mainNodeLabelBlockHeightPx(label);
50
+ return WORKFLOW_CANVAS_MAIN_NODE_CARD_PX + WORKFLOW_CANVAS_MAIN_NODE_LABEL_GAP_PX + badge + labelBelowCard;
51
+ }
52
+
53
+ static attachmentNodeWidthPx(): number {
54
+ return WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX;
55
+ }
56
+
57
+ static attachmentNodeLabelBlockHeightPx(label: string): number {
58
+ const maxW = Math.max(28, WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX - WORKFLOW_CANVAS_LABEL_HORIZONTAL_PADDING_PX);
59
+ const lines = WorkflowCanvasLabelLayoutEstimator.estimateLineCount(
60
+ label,
61
+ maxW,
62
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_FONT_PX,
63
+ );
64
+ const lineHeightPx =
65
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_FONT_PX * WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_LINE_HEIGHT;
66
+ return lines * lineHeightPx + 2;
67
+ }
68
+
69
+ static attachmentNodeHeightPx(label: string): number {
70
+ return (
71
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX +
72
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_LABEL_GAP_PX +
73
+ this.attachmentNodeLabelBlockHeightPx(label)
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Vertical distance from the bottom of the main node card to the bottom of the shell content
79
+ * (gap, optional agent badge row, label block). Used to place attachment card centers from the
80
+ * parent card center when Dagre aligns by card square only.
81
+ */
82
+ static verticalExtentBelowParentMainCard(parentLabel: string, parentIsAgent: boolean): number {
83
+ const badge = parentIsAgent ? WORKFLOW_CANVAS_AGENT_BADGE_ROW_PX : 0;
84
+ const labelBelowCard = parentIsAgent ? 0 : this.mainNodeLabelBlockHeightPx(parentLabel);
85
+ return WORKFLOW_CANVAS_MAIN_NODE_LABEL_GAP_PX + badge + labelBelowCard;
86
+ }
87
+
88
+ /**
89
+ * Delta Y from parent main-node **card center** to child attachment **card center** (siblings share one row).
90
+ */
91
+ static attachmentCardCenterYDeltaFromParentCardCenter(parentLabel: string, parentIsAgent: boolean): number {
92
+ return (
93
+ WORKFLOW_CANVAS_MAIN_NODE_CARD_PX / 2 +
94
+ this.verticalExtentBelowParentMainCard(parentLabel, parentIsAgent) +
95
+ WORKFLOW_CANVAS_ATTACHMENT_STACK_GAP_PX +
96
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX / 2
97
+ );
98
+ }
99
+ }
@@ -0,0 +1,46 @@
1
+ import { CircleAlert, CircleCheckBig, Clock3, Pin } from "lucide-react";
2
+
3
+ import type { NodeExecutionSnapshot } from "../../hooks/realtime/realtime";
4
+
5
+ export function statusIconForNode(status: NodeExecutionSnapshot["status"] | undefined) {
6
+ if (status === "completed") {
7
+ return <CircleCheckBig size={15} style={{ color: "#15803d" }} strokeWidth={2.1} />;
8
+ }
9
+ if (status === "skipped") {
10
+ return <Clock3 size={15} style={{ color: "#d97706" }} strokeWidth={2.1} />;
11
+ }
12
+ if (status === "failed") {
13
+ return <CircleAlert size={15} style={{ color: "#b91c1c" }} strokeWidth={2.1} />;
14
+ }
15
+ if (status === "running" || status === "queued" || status === "pending" || typeof status === "undefined") {
16
+ return null;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ export function trailingIconForNode(
22
+ args: Readonly<{ status: NodeExecutionSnapshot["status"] | undefined; isPinned: boolean }>,
23
+ ) {
24
+ if (args.isPinned) {
25
+ return <Pin size={14} style={{ color: "#6d28d9" }} strokeWidth={2.4} fill="currentColor" />;
26
+ }
27
+ return statusIconForNode(args.status);
28
+ }
29
+
30
+ export function trailingIconKindForNode(
31
+ args: Readonly<{ status: NodeExecutionSnapshot["status"] | undefined; isPinned: boolean }>,
32
+ ): string {
33
+ if (args.isPinned) {
34
+ return "pin";
35
+ }
36
+ if (args.status === "completed") {
37
+ return "completed";
38
+ }
39
+ if (args.status === "skipped") {
40
+ return "skipped";
41
+ }
42
+ if (args.status === "failed") {
43
+ return "failed";
44
+ }
45
+ return "none";
46
+ }
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { createContext } from "react";
4
+
5
+ import type { RetainWorkflowSubscription } from "../../lib/realtime/realtimeClientBridge";
6
+
7
+ export type RealtimeContextValue = Readonly<{
8
+ retainWorkflowSubscription: RetainWorkflowSubscription;
9
+ isConnected: boolean;
10
+ /** True when the workflow websocket transport is closed (not while connecting or before the first socket opens). */
11
+ showDisconnectedBadge: boolean;
12
+ }>;
13
+
14
+ export const RealtimeContext = createContext<RealtimeContextValue | null>(null);
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import type { Logger } from "@codemation/host-src/application/logging/Logger";
4
+ import type { ReactNode } from "react";
5
+
6
+ import { RealtimeContext } from "./RealtimeContext";
7
+ import { useWorkflowRealtimeInfrastructure } from "../../hooks/realtime/useWorkflowRealtimeInfrastructure";
8
+
9
+ export function WorkflowRealtimeProvider(
10
+ args: Readonly<{ children: ReactNode; logger: Logger; websocketPort?: string }>,
11
+ ) {
12
+ const { children, logger, websocketPort } = args;
13
+ const value = useWorkflowRealtimeInfrastructure({ logger, websocketPort });
14
+ return <RealtimeContext.Provider value={value}>{children}</RealtimeContext.Provider>;
15
+ }