@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,28 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+
5
+ import type { ReactNode } from "react";
6
+
7
+ import type { WorkflowSummary } from "../hooks/realtime/realtime";
8
+
9
+ import { WorkflowFolderTreeBuilder } from "@/shell/WorkflowFolderTreeBuilder";
10
+
11
+ import { WorkflowListRoot } from "./WorkflowListRoot";
12
+
13
+ const treeBuilder = new WorkflowFolderTreeBuilder();
14
+
15
+ export function WorkflowsListTree(args: Readonly<{ workflows: ReadonlyArray<WorkflowSummary> }>): ReactNode {
16
+ const pathname = usePathname();
17
+ const tree = treeBuilder.build(args.workflows);
18
+ return (
19
+ <div
20
+ className="rounded-xl border border-border/50 bg-gradient-to-b from-card/90 to-muted/15 p-3 shadow-sm ring-1 ring-black/[0.04] dark:from-card/70 dark:to-muted/25 dark:ring-white/[0.05] sm:p-4"
21
+ data-testid="workflows-list"
22
+ >
23
+ <ul className="m-0 grid list-none gap-1.5 p-0">
24
+ <WorkflowListRoot node={tree} pathname={pathname} workflows={args.workflows} />
25
+ </ul>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, useCallback, useLayoutEffect, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ /**
7
+ * Floating tooltip for canvas chrome (portal to `document.body` so parent `overflow:hidden` does not clip).
8
+ * Positioned below the anchor so header-row icons (e.g. next to Active) are not clipped by the viewport top edge.
9
+ */
10
+ export function CanvasNodeChromeTooltip(
11
+ args: Readonly<{
12
+ testId: string;
13
+ ariaLabel: string;
14
+ tooltip: string;
15
+ children: ReactNode;
16
+ }>,
17
+ ): React.JSX.Element {
18
+ const { ariaLabel, children, testId, tooltip } = args;
19
+ const anchorRef = useRef<HTMLDivElement>(null);
20
+ const [visible, setVisible] = useState(false);
21
+ const [bubble, setBubble] = useState<Readonly<{ x: number; y: number }> | null>(null);
22
+
23
+ const updatePosition = useCallback(() => {
24
+ const el = anchorRef.current;
25
+ if (!el) {
26
+ return;
27
+ }
28
+ const r = el.getBoundingClientRect();
29
+ setBubble({ x: r.left + r.width / 2, y: r.bottom });
30
+ }, []);
31
+
32
+ useLayoutEffect(() => {
33
+ if (visible) {
34
+ updatePosition();
35
+ }
36
+ }, [visible, updatePosition]);
37
+
38
+ useLayoutEffect(() => {
39
+ if (!visible) {
40
+ return;
41
+ }
42
+ const onScroll = (): void => {
43
+ updatePosition();
44
+ };
45
+ window.addEventListener("scroll", onScroll, true);
46
+ window.addEventListener("resize", onScroll);
47
+ return () => {
48
+ window.removeEventListener("scroll", onScroll, true);
49
+ window.removeEventListener("resize", onScroll);
50
+ };
51
+ }, [visible, updatePosition]);
52
+
53
+ const tooltipBubble =
54
+ visible && bubble && typeof document !== "undefined" ? (
55
+ <div
56
+ role="tooltip"
57
+ className="pointer-events-none fixed z-[10000] max-w-[min(22rem,calc(100vw-2rem))] px-2 py-1.5 text-left text-[11px] font-bold leading-snug whitespace-pre-wrap text-white shadow-lg"
58
+ style={{
59
+ left: bubble.x,
60
+ top: bubble.y,
61
+ transform: "translate(-50%, 8px)",
62
+ background: "rgba(15,23,42,0.94)",
63
+ boxShadow: "0 10px 24px rgba(15,23,42,0.2)",
64
+ }}
65
+ >
66
+ {tooltip}
67
+ </div>
68
+ ) : null;
69
+
70
+ return (
71
+ <div
72
+ ref={anchorRef}
73
+ data-testid={testId}
74
+ className="relative inline-flex"
75
+ onPointerEnter={() => {
76
+ updatePosition();
77
+ setVisible(true);
78
+ }}
79
+ onPointerLeave={() => setVisible(false)}
80
+ onFocusCapture={() => {
81
+ updatePosition();
82
+ setVisible(true);
83
+ }}
84
+ onBlurCapture={(event) => {
85
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
86
+ setVisible(false);
87
+ }
88
+ }}
89
+ >
90
+ <span aria-label={ariaLabel} className="inline-flex">
91
+ {children}
92
+ </span>
93
+ {tooltipBubble ? createPortal(tooltipBubble, document.body) : null}
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Fixed square slot so Lucide, Simple Icons, brand SVGs, and raster URLs share one bounding box.
5
+ */
6
+ export function CanvasNodeIconSlot(props: Readonly<{ sizePx: number; children: ReactNode }>) {
7
+ const { sizePx, children } = props;
8
+ return (
9
+ <span
10
+ style={{
11
+ width: sizePx,
12
+ height: sizePx,
13
+ display: "inline-flex",
14
+ alignItems: "center",
15
+ justifyContent: "center",
16
+ flexShrink: 0,
17
+ lineHeight: 0,
18
+ color: "#0f172a",
19
+ backgroundColor: "transparent",
20
+ }}
21
+ >
22
+ {children}
23
+ </span>
24
+ );
25
+ }
@@ -0,0 +1,84 @@
1
+ import type { ConnectionInvocationRecord, NodeExecutionSnapshot } from "../../hooks/realtime/realtime";
2
+
3
+ export class VisibleNodeStatusResolver {
4
+ private static readonly statusPriorityByStatus = new Map<NodeExecutionSnapshot["status"], number>([
5
+ ["running", 0],
6
+ ["queued", 1],
7
+ ["completed", 2],
8
+ ["failed", 3],
9
+ ["skipped", 4],
10
+ ["pending", 5],
11
+ ]);
12
+
13
+ private static readonly invocationWorstStatusOrder = [
14
+ "failed",
15
+ "running",
16
+ "queued",
17
+ "completed",
18
+ "skipped",
19
+ "pending",
20
+ ] as const;
21
+
22
+ static resolveStatuses(
23
+ nodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>,
24
+ connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>,
25
+ ): Readonly<Record<string, NodeExecutionSnapshot["status"] | undefined>> {
26
+ const snapshotsByVisibleNodeId = new Map<string, NodeExecutionSnapshot[]>();
27
+ for (const [nodeId, snapshot] of Object.entries(nodeSnapshotsByNodeId)) {
28
+ const snapshots = snapshotsByVisibleNodeId.get(nodeId) ?? [];
29
+ snapshots.push(snapshot);
30
+ snapshotsByVisibleNodeId.set(nodeId, snapshots);
31
+ }
32
+
33
+ const statusEntries: Array<readonly [string, NodeExecutionSnapshot["status"]]> = [];
34
+ for (const [visibleNodeId, snapshots] of snapshotsByVisibleNodeId.entries()) {
35
+ const resolvedSnapshot = [...snapshots].sort((left, right) => this.compareSnapshots(left, right))[0];
36
+ if (resolvedSnapshot) {
37
+ statusEntries.push([visibleNodeId, resolvedSnapshot.status] as const);
38
+ }
39
+ }
40
+ const result = Object.fromEntries(statusEntries) as Record<string, NodeExecutionSnapshot["status"] | undefined>;
41
+ const invocationsByConnectionNodeId = new Map<string, ConnectionInvocationRecord[]>();
42
+ for (const inv of connectionInvocations ?? []) {
43
+ const list = invocationsByConnectionNodeId.get(inv.connectionNodeId) ?? [];
44
+ list.push(inv);
45
+ invocationsByConnectionNodeId.set(inv.connectionNodeId, list);
46
+ }
47
+ for (const [connectionNodeId, invs] of invocationsByConnectionNodeId.entries()) {
48
+ const aggregated = this.worstInvocationStatus(invs.map((entry) => entry.status));
49
+ if (aggregated) {
50
+ result[connectionNodeId] = aggregated;
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+
56
+ private static worstInvocationStatus(
57
+ statuses: ReadonlyArray<NodeExecutionSnapshot["status"]>,
58
+ ): NodeExecutionSnapshot["status"] | undefined {
59
+ if (statuses.length === 0) {
60
+ return undefined;
61
+ }
62
+ let best: NodeExecutionSnapshot["status"] | undefined;
63
+ let bestIdx: number = this.invocationWorstStatusOrder.length;
64
+ for (const status of statuses) {
65
+ const idx = this.invocationWorstStatusOrder.indexOf(status);
66
+ const resolvedIdx = idx >= 0 ? idx : this.invocationWorstStatusOrder.length;
67
+ if (resolvedIdx < bestIdx) {
68
+ bestIdx = resolvedIdx;
69
+ best = status;
70
+ }
71
+ }
72
+ return best ?? statuses[0];
73
+ }
74
+
75
+ private static compareSnapshots(left: NodeExecutionSnapshot, right: NodeExecutionSnapshot): number {
76
+ const statusPriorityComparison = this.getStatusPriority(left.status) - this.getStatusPriority(right.status);
77
+ if (statusPriorityComparison !== 0) return statusPriorityComparison;
78
+ return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
79
+ }
80
+
81
+ private static getStatusPriority(status: NodeExecutionSnapshot["status"]): number {
82
+ return this.statusPriorityByStatus.get(status) ?? Number.MAX_SAFE_INTEGER;
83
+ }
84
+ }
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ import {
4
+ Background,
5
+ Controls,
6
+ ReactFlow,
7
+ type Edge as ReactFlowEdge,
8
+ type ReactFlowInstance,
9
+ type Node as ReactFlowNode,
10
+ } from "@xyflow/react";
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
+
13
+ import type { ConnectionInvocationRecord, NodeExecutionSnapshot } from "../../hooks/realtime/realtime";
14
+ import type { WorkflowDto } from "../../lib/realtime/workflowTypes";
15
+ import { layoutWorkflow } from "./lib/layoutWorkflow";
16
+ import type { WorkflowCanvasNodeData } from "./lib/workflowCanvasNodeData";
17
+ import { workflowCanvasEdgeTypes, workflowCanvasNodeTypes } from "./lib/workflowCanvasFlowTypes";
18
+ import { useWorkflowCanvasVisibleNodeStatuses } from "../../hooks/canvas/useWorkflowCanvasVisibleNodeStatuses";
19
+ import { WORKFLOW_CANVAS_EMBEDDED_STYLES } from "./lib/workflowCanvasEmbeddedStyles";
20
+ import { WorkflowCanvasLoadingPlaceholder } from "./WorkflowCanvasLoadingPlaceholder";
21
+ import { WorkflowCanvasStructureSignature } from "./WorkflowCanvasStructureSignature";
22
+
23
+ export function WorkflowCanvas(args: {
24
+ workflow: WorkflowDto;
25
+ nodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>;
26
+ connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
27
+ credentialAttentionTooltipByNodeId?: ReadonlyMap<string, string>;
28
+ selectedNodeId: string | null;
29
+ propertiesTargetNodeId: string | null;
30
+ pinnedNodeIds?: ReadonlySet<string>;
31
+ isLiveWorkflowView: boolean;
32
+ isRunning: boolean;
33
+ onSelectNode: (nodeId: string) => void;
34
+ onOpenPropertiesNode: (nodeId: string) => void;
35
+ onRunNode: (nodeId: string) => void;
36
+ onTogglePinnedOutput: (nodeId: string) => void;
37
+ onEditNodeOutput: (nodeId: string) => void;
38
+ onClearPinnedOutput: (nodeId: string) => void;
39
+ workflowNodeIdsWithBoundCredential?: ReadonlySet<string>;
40
+ onRequestOpenCredentialEditForNode?: (nodeId: string) => void;
41
+ }) {
42
+ const {
43
+ workflow,
44
+ nodeSnapshotsByNodeId,
45
+ connectionInvocations = [],
46
+ credentialAttentionTooltipByNodeId = new Map<string, string>(),
47
+ selectedNodeId,
48
+ propertiesTargetNodeId,
49
+ pinnedNodeIds = new Set<string>(),
50
+ isLiveWorkflowView,
51
+ isRunning,
52
+ onSelectNode,
53
+ onOpenPropertiesNode,
54
+ onRunNode,
55
+ onTogglePinnedOutput,
56
+ onEditNodeOutput,
57
+ onClearPinnedOutput,
58
+ workflowNodeIdsWithBoundCredential = new Set<string>(),
59
+ onRequestOpenCredentialEditForNode = () => {},
60
+ } = args;
61
+ const [hasMountedOnClient, setHasMountedOnClient] = useState(false);
62
+ const [isInitialViewportReady, setIsInitialViewportReady] = useState(false);
63
+ const workflowStructureSignature = useMemo(() => WorkflowCanvasStructureSignature.create(workflow), [workflow]);
64
+ const visibleNodeStatusesByNodeId = useWorkflowCanvasVisibleNodeStatuses(
65
+ nodeSnapshotsByNodeId,
66
+ connectionInvocations,
67
+ );
68
+ const { nodes, edges } = useMemo(
69
+ () =>
70
+ layoutWorkflow(
71
+ workflow,
72
+ nodeSnapshotsByNodeId,
73
+ connectionInvocations,
74
+ visibleNodeStatusesByNodeId,
75
+ credentialAttentionTooltipByNodeId,
76
+ selectedNodeId,
77
+ propertiesTargetNodeId,
78
+ pinnedNodeIds,
79
+ isLiveWorkflowView,
80
+ isRunning,
81
+ workflowNodeIdsWithBoundCredential,
82
+ onSelectNode,
83
+ onOpenPropertiesNode,
84
+ onRequestOpenCredentialEditForNode,
85
+ onRunNode,
86
+ onTogglePinnedOutput,
87
+ onEditNodeOutput,
88
+ onClearPinnedOutput,
89
+ ),
90
+ [
91
+ connectionInvocations,
92
+ credentialAttentionTooltipByNodeId,
93
+ isLiveWorkflowView,
94
+ isRunning,
95
+ nodeSnapshotsByNodeId,
96
+ onClearPinnedOutput,
97
+ onEditNodeOutput,
98
+ onOpenPropertiesNode,
99
+ onRequestOpenCredentialEditForNode,
100
+ onRunNode,
101
+ onSelectNode,
102
+ onTogglePinnedOutput,
103
+ pinnedNodeIds,
104
+ propertiesTargetNodeId,
105
+ selectedNodeId,
106
+ visibleNodeStatusesByNodeId,
107
+ workflow,
108
+ workflowNodeIdsWithBoundCredential,
109
+ ],
110
+ );
111
+ const canvasContainerRef = useRef<HTMLDivElement | null>(null);
112
+ const reactFlowInstanceRef = useRef<ReactFlowInstance<ReactFlowNode<WorkflowCanvasNodeData>, ReactFlowEdge> | null>(
113
+ null,
114
+ );
115
+ const fitViewAnimationFrameIdRef = useRef<number | null>(null);
116
+ const fitViewTimeoutIdRef = useRef<number | null>(null);
117
+ const fitViewRequestIdRef = useRef(0);
118
+ const fitViewOptions = useMemo(
119
+ () =>
120
+ ({
121
+ padding: 0.24,
122
+ minZoom: 0.2,
123
+ maxZoom: 1,
124
+ }) as const,
125
+ [],
126
+ );
127
+ const scheduleFitView = useCallback(() => {
128
+ const canvasContainer = canvasContainerRef.current;
129
+ const reactFlowInstance = reactFlowInstanceRef.current;
130
+ if (!canvasContainer || !reactFlowInstance || nodes.length === 0) {
131
+ return;
132
+ }
133
+ if (canvasContainer.clientWidth === 0 || canvasContainer.clientHeight === 0) {
134
+ return;
135
+ }
136
+ if (fitViewAnimationFrameIdRef.current !== null) {
137
+ cancelAnimationFrame(fitViewAnimationFrameIdRef.current);
138
+ }
139
+ fitViewRequestIdRef.current += 1;
140
+ const requestId = fitViewRequestIdRef.current;
141
+ fitViewAnimationFrameIdRef.current = requestAnimationFrame(() => {
142
+ fitViewAnimationFrameIdRef.current = requestAnimationFrame(() => {
143
+ fitViewAnimationFrameIdRef.current = null;
144
+ void reactFlowInstance.fitView(fitViewOptions).then(() => {
145
+ if (requestId !== fitViewRequestIdRef.current) {
146
+ return;
147
+ }
148
+ setIsInitialViewportReady(true);
149
+ });
150
+ });
151
+ });
152
+ }, [fitViewOptions, nodes.length]);
153
+
154
+ useEffect(() => {
155
+ setHasMountedOnClient(true);
156
+ }, []);
157
+
158
+ useEffect(() => {
159
+ setIsInitialViewportReady(false);
160
+ }, [workflow.id, workflowStructureSignature]);
161
+
162
+ useEffect(() => {
163
+ scheduleFitView();
164
+ if (fitViewTimeoutIdRef.current !== null) {
165
+ window.clearTimeout(fitViewTimeoutIdRef.current);
166
+ }
167
+ fitViewTimeoutIdRef.current = window.setTimeout(() => {
168
+ fitViewTimeoutIdRef.current = null;
169
+ scheduleFitView();
170
+ }, 120);
171
+ }, [scheduleFitView, workflow.id, workflowStructureSignature]);
172
+
173
+ useEffect(() => {
174
+ const canvasContainer = canvasContainerRef.current;
175
+ if (!canvasContainer || typeof ResizeObserver === "undefined") {
176
+ return;
177
+ }
178
+ const resizeObserver = new ResizeObserver(() => {
179
+ scheduleFitView();
180
+ });
181
+ resizeObserver.observe(canvasContainer);
182
+ return () => {
183
+ resizeObserver.disconnect();
184
+ };
185
+ }, [scheduleFitView]);
186
+
187
+ useEffect(() => {
188
+ return () => {
189
+ if (fitViewAnimationFrameIdRef.current !== null) {
190
+ cancelAnimationFrame(fitViewAnimationFrameIdRef.current);
191
+ }
192
+ if (fitViewTimeoutIdRef.current !== null) {
193
+ window.clearTimeout(fitViewTimeoutIdRef.current);
194
+ }
195
+ };
196
+ }, []);
197
+
198
+ return (
199
+ <div
200
+ ref={canvasContainerRef}
201
+ data-testid="workflow-canvas-root"
202
+ data-workflow-structure-signature={workflowStructureSignature}
203
+ style={{
204
+ width: "100%",
205
+ height: "100%",
206
+ background: "#fbfbfc",
207
+ fontFamily: "inherit",
208
+ position: "relative",
209
+ }}
210
+ >
211
+ {hasMountedOnClient ? (
212
+ <ReactFlow
213
+ nodes={nodes}
214
+ edges={edges}
215
+ nodeTypes={workflowCanvasNodeTypes}
216
+ edgeTypes={workflowCanvasEdgeTypes}
217
+ onInit={(instance) => {
218
+ reactFlowInstanceRef.current = instance;
219
+ scheduleFitView();
220
+ }}
221
+ onNodeClick={(_event, node) => {
222
+ onSelectNode(node.id);
223
+ onOpenPropertiesNode(node.id);
224
+ }}
225
+ style={{
226
+ fontFamily: "inherit",
227
+ opacity: isInitialViewportReady ? 1 : 0,
228
+ transition: "opacity 120ms ease-out",
229
+ }}
230
+ nodesDraggable={false}
231
+ nodesConnectable={false}
232
+ elementsSelectable
233
+ zoomOnScroll
234
+ panOnScroll
235
+ >
236
+ <Background gap={22} size={1.1} color="#d9e0ea" />
237
+ <Controls showInteractive={false} position="bottom-left" />
238
+ </ReactFlow>
239
+ ) : null}
240
+ <WorkflowCanvasLoadingPlaceholder isInitialViewportReady={isInitialViewportReady} />
241
+ <style>{WORKFLOW_CANVAS_EMBEDDED_STYLES}</style>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ export { VisibleNodeStatusResolver } from "./VisibleNodeStatusResolver";
247
+ export { WorkflowCanvasEdgeCountResolver } from "./lib/WorkflowCanvasEdgeCountResolver";
248
+ export { WorkflowCanvasStructureSignature } from "./WorkflowCanvasStructureSignature";
@@ -0,0 +1,182 @@
1
+ import { type CSSProperties, useEffect, useRef, useState } from "react";
2
+
3
+ import type { WorkflowCanvasNodeData } from "./lib/workflowCanvasNodeData";
4
+ import {
5
+ WORKFLOW_CANVAS_AGENT_NODE_CARD_WIDTH_PX,
6
+ WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX,
7
+ WORKFLOW_CANVAS_MAIN_NODE_CARD_PX,
8
+ WORKFLOW_CANVAS_MAIN_NODE_LABEL_GAP_PX,
9
+ WorkflowCanvasNodeGeometry,
10
+ } from "./lib/workflowCanvasNodeGeometry";
11
+ import { WorkflowCanvasCodemationNodeAccents } from "./WorkflowCanvasCodemationNodeAccents";
12
+ import { WorkflowCanvasCodemationNodeAgentLabels } from "./WorkflowCanvasCodemationNodeAgentLabels";
13
+ import { WorkflowCanvasCodemationNodeCard } from "./WorkflowCanvasCodemationNodeCard";
14
+ import { WorkflowCanvasCodemationNodeAgentBottomSourceHandles } from "./WorkflowCanvasCodemationNodeAgentBottomSourceHandles";
15
+ import { WorkflowCanvasCodemationNodeHandles } from "./WorkflowCanvasCodemationNodeHandles";
16
+ import { WorkflowCanvasCodemationNodeLabelBelow } from "./WorkflowCanvasCodemationNodeLabelBelow";
17
+ import { WorkflowCanvasCodemationNodeToolbar } from "./WorkflowCanvasCodemationNodeToolbar";
18
+
19
+ export function CodemationNode({ data }: { data: WorkflowCanvasNodeData }) {
20
+ const isQueued = data.status === "queued";
21
+ const isRunning = data.status === "running";
22
+ const isActive = isQueued || isRunning;
23
+ const isSelected = data.selected;
24
+ const isPropertiesTarget = data.propertiesTarget;
25
+ const isAttachment = data.isAttachment;
26
+ const isAgent = data.role === "agent";
27
+ const isPinned = data.isPinned;
28
+ const [isHovered, setIsHovered] = useState(false);
29
+ const [hasToolbarFocus, setHasToolbarFocus] = useState(false);
30
+ const hideToolbarTimeoutRef = useRef<number | null>(null);
31
+ const showsCanvasControls = data.isLiveWorkflowView && !isAttachment;
32
+ const isToolbarVisible = showsCanvasControls && (isHovered || hasToolbarFocus);
33
+ const activityColor = isRunning ? "#2563eb" : "#7c3aed";
34
+ const cardHeightPx = isAttachment ? WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX : WORKFLOW_CANVAS_MAIN_NODE_CARD_PX;
35
+ const cardWidthPx = isAttachment
36
+ ? WORKFLOW_CANVAS_ATTACHMENT_NODE_CARD_PX
37
+ : isAgent
38
+ ? WORKFLOW_CANVAS_AGENT_NODE_CARD_WIDTH_PX
39
+ : WORKFLOW_CANVAS_MAIN_NODE_CARD_PX;
40
+ const fallbackWidthPx = isAttachment
41
+ ? WorkflowCanvasNodeGeometry.attachmentNodeWidthPx()
42
+ : WorkflowCanvasNodeGeometry.mainNodeWidthPx(isAgent);
43
+ const fallbackHeightPx = isAttachment
44
+ ? WorkflowCanvasNodeGeometry.attachmentNodeHeightPx(data.label)
45
+ : WorkflowCanvasNodeGeometry.mainNodeHeightPx(data.label, isAgent);
46
+ const nodeWidthPx = data.layoutWidthPx > 0 ? data.layoutWidthPx : fallbackWidthPx;
47
+ const nodeHeightPx = data.layoutHeightPx > 0 ? data.layoutHeightPx : fallbackHeightPx;
48
+ const attachmentSourceOffsetFromNodeBottomPx = Math.max(0, Math.round(nodeHeightPx - cardHeightPx));
49
+ const activityRingStyle: CSSProperties = {
50
+ position: "absolute",
51
+ inset: -4,
52
+ borderRadius: 9,
53
+ pointerEvents: "none",
54
+ opacity: isRunning ? 1 : 0.75,
55
+ padding: 2,
56
+ background: `conic-gradient(from var(--codemation-node-ring-angle), ${activityColor} 0deg, ${activityColor} 72deg, ${activityColor}22 132deg, ${activityColor}1f 228deg, ${activityColor} 324deg, ${activityColor} 360deg)`,
57
+ animation: isRunning
58
+ ? "codemationNodeRingRotate 1.5s linear infinite"
59
+ : "codemationNodeRingRotate 4.5s linear infinite",
60
+ WebkitMask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
61
+ WebkitMaskComposite: "xor",
62
+ maskComposite: "exclude",
63
+ ["--codemation-node-ring-angle" as string]: "0deg",
64
+ };
65
+ useEffect(() => {
66
+ return () => {
67
+ if (hideToolbarTimeoutRef.current !== null) {
68
+ window.clearTimeout(hideToolbarTimeoutRef.current);
69
+ }
70
+ };
71
+ }, []);
72
+ return (
73
+ <div
74
+ onPointerEnter={() => {
75
+ if (hideToolbarTimeoutRef.current !== null) {
76
+ window.clearTimeout(hideToolbarTimeoutRef.current);
77
+ hideToolbarTimeoutRef.current = null;
78
+ }
79
+ setIsHovered(true);
80
+ }}
81
+ onPointerLeave={() => {
82
+ if (hideToolbarTimeoutRef.current !== null) {
83
+ window.clearTimeout(hideToolbarTimeoutRef.current);
84
+ }
85
+ hideToolbarTimeoutRef.current = window.setTimeout(() => {
86
+ setIsHovered(false);
87
+ hideToolbarTimeoutRef.current = null;
88
+ }, 140);
89
+ }}
90
+ onFocusCapture={() => {
91
+ if (hideToolbarTimeoutRef.current !== null) {
92
+ window.clearTimeout(hideToolbarTimeoutRef.current);
93
+ hideToolbarTimeoutRef.current = null;
94
+ }
95
+ setHasToolbarFocus(true);
96
+ }}
97
+ onBlurCapture={(event) => {
98
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
99
+ if (hideToolbarTimeoutRef.current !== null) {
100
+ window.clearTimeout(hideToolbarTimeoutRef.current);
101
+ }
102
+ hideToolbarTimeoutRef.current = window.setTimeout(() => {
103
+ setHasToolbarFocus(false);
104
+ hideToolbarTimeoutRef.current = null;
105
+ }, 140);
106
+ }
107
+ }}
108
+ style={{
109
+ width: nodeWidthPx,
110
+ height: nodeHeightPx,
111
+ borderRadius: 0,
112
+ background: "transparent",
113
+ boxShadow: "none",
114
+ position: "relative",
115
+ overflow: "visible",
116
+ }}
117
+ data-testid={`canvas-node-shell-${data.nodeId}`}
118
+ >
119
+ <div
120
+ onClick={(event) => {
121
+ event.stopPropagation();
122
+ data.onSelectNode(data.nodeId);
123
+ data.onOpenPropertiesNode(data.nodeId);
124
+ }}
125
+ style={{
126
+ display: "flex",
127
+ flexDirection: "column",
128
+ alignItems: "center",
129
+ width: nodeWidthPx,
130
+ }}
131
+ >
132
+ <div
133
+ style={{
134
+ position: "relative",
135
+ width: cardWidthPx,
136
+ height: cardHeightPx,
137
+ borderRadius: 7,
138
+ }}
139
+ >
140
+ <WorkflowCanvasCodemationNodeAccents
141
+ activityColor={activityColor}
142
+ activityRingStyle={activityRingStyle}
143
+ isActive={isActive}
144
+ isActiveForProperties={isActive}
145
+ isActiveForSelected={isActive}
146
+ isPropertiesTarget={isPropertiesTarget}
147
+ isRunning={isRunning}
148
+ isSelected={isSelected}
149
+ />
150
+ <WorkflowCanvasCodemationNodeHandles
151
+ kind={data.kind}
152
+ isAgent={isAgent}
153
+ isAttachment={isAttachment}
154
+ omitAgentBottomSourceHandles={isAgent && !isAttachment}
155
+ sourceOutputPorts={data.sourceOutputPorts}
156
+ targetInputPorts={data.targetInputPorts}
157
+ />
158
+ <WorkflowCanvasCodemationNodeCard cardWidthPx={cardWidthPx} cardHeightPx={cardHeightPx} data={data} />
159
+ </div>
160
+ {isAgent ? (
161
+ <div style={{ marginTop: WORKFLOW_CANVAS_MAIN_NODE_LABEL_GAP_PX, width: "100%" }}>
162
+ <WorkflowCanvasCodemationNodeAgentLabels />
163
+ </div>
164
+ ) : null}
165
+ <WorkflowCanvasCodemationNodeLabelBelow data={data} maxWidthPx={cardWidthPx} />
166
+ </div>
167
+ {isAgent && !isAttachment ? (
168
+ <WorkflowCanvasCodemationNodeAgentBottomSourceHandles
169
+ offsetFromNodeBottomPx={attachmentSourceOffsetFromNodeBottomPx}
170
+ />
171
+ ) : null}
172
+ {showsCanvasControls ? (
173
+ <WorkflowCanvasCodemationNodeToolbar
174
+ data={data}
175
+ isPinned={isPinned}
176
+ isToolbarVisible={isToolbarVisible}
177
+ setHasToolbarFocus={setHasToolbarFocus}
178
+ />
179
+ ) : null}
180
+ </div>
181
+ );
182
+ }