@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,1300 @@
1
+ import { useQueryClient } from "@tanstack/react-query";
2
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ import { WorkflowActivationHttpErrorFormat } from "../../lib/workflowDetail/WorkflowActivationHttpErrorFormat";
6
+ import {
7
+ useRunQuery,
8
+ useWorkflowCredentialHealthQuery,
9
+ useWorkflowDebuggerOverlayQuery,
10
+ useWorkflowDevBuildStateQuery,
11
+ useWorkflowQuery,
12
+ useWorkflowRealtimeSubscription,
13
+ useSetWorkflowActivationMutation,
14
+ useWorkflowRunsQuery,
15
+ type Items,
16
+ type NodeExecutionSnapshot,
17
+ type PersistedRunState,
18
+ type RunSummary,
19
+ type WorkflowDevBuildState,
20
+ type WorkflowDto,
21
+ } from "../realtime/realtime";
22
+ import { useWorkflowRealtimeShowDisconnectedBadge } from "../realtime/useWorkflowRealtimeShowDisconnectedBadge";
23
+ import { WorkflowDetailPresenter, type RunWorkflowRequest } from "../../lib/workflowDetail/WorkflowDetailPresenter";
24
+ import {
25
+ WorkflowDetailUrlCodec,
26
+ type WorkflowDetailUrlLocation,
27
+ } from "../../lib/workflowDetail/WorkflowDetailUrlCodec";
28
+ import type {
29
+ InspectorFormat,
30
+ InspectorMode,
31
+ InspectorTab,
32
+ JsonEditorState,
33
+ PinBinaryMapsByItemIndex,
34
+ WorkflowExecutionInspectorActions,
35
+ WorkflowExecutionInspectorFormatting,
36
+ WorkflowExecutionInspectorModel,
37
+ WorkflowRunsSidebarActions,
38
+ WorkflowRunsSidebarFormatting,
39
+ WorkflowRunsSidebarModel,
40
+ } from "../../lib/workflowDetail/workflowDetailTypes";
41
+
42
+ export type WorkflowDetailControllerResult = Readonly<{
43
+ displayedWorkflow: WorkflowDto | undefined;
44
+ displayedNodeSnapshotsByNodeId: Readonly<Record<string, NodeExecutionSnapshot>>;
45
+ displayedConnectionInvocations: ReadonlyArray<NonNullable<PersistedRunState["connectionInvocations"]>[number]>;
46
+ pinnedNodeIds: ReadonlySet<string>;
47
+ isLiveWorkflowView: boolean;
48
+ isRunsPaneVisible: boolean;
49
+ isRunning: boolean;
50
+ workflowDevBuildState: WorkflowDevBuildState;
51
+ showRealtimeDisconnectedBadge: boolean;
52
+ canCopySelectedRunToLive: boolean;
53
+ /** Nodes on the canvas that have a required credential slot with status unbound. */
54
+ credentialAttentionNodeIds: ReadonlySet<string>;
55
+ /** Lines for workflow-level credential attention tooltip (node label · slot label). */
56
+ credentialAttentionSummaryLines: ReadonlyArray<string>;
57
+ /** Per-canvas-node tooltip lines for unbound required credential slots. */
58
+ credentialAttentionTooltipByNodeId: ReadonlyMap<string, string>;
59
+ /** Nodes that have at least one bound credential instance (canvas toolbar can open edit). */
60
+ workflowNodeIdsWithBoundCredential: ReadonlySet<string>;
61
+ selectedRun: PersistedRunState | undefined;
62
+ sidebarModel: WorkflowRunsSidebarModel;
63
+ sidebarFormatting: WorkflowRunsSidebarFormatting;
64
+ sidebarActions: WorkflowRunsSidebarActions;
65
+ inspectorModel: WorkflowExecutionInspectorModel;
66
+ inspectorFormatting: WorkflowExecutionInspectorFormatting;
67
+ inspectorActions: WorkflowExecutionInspectorActions;
68
+ selectedNodeId: string | null;
69
+ propertiesPanelNodeId: string | null;
70
+ isPropertiesPanelOpen: boolean;
71
+ selectedPropertiesWorkflowNode: WorkflowDto["nodes"][number] | undefined;
72
+ selectCanvasNode: (nodeId: string) => void;
73
+ openPropertiesPanelForNode: (nodeId: string) => void;
74
+ /** Opens the properties panel and requests the credential edit dialog for the first bound slot on that node. */
75
+ requestOpenCredentialEditForNode: (nodeId: string) => void;
76
+ pendingCredentialEditForNodeId: string | null;
77
+ consumePendingCredentialEditRequest: () => void;
78
+ closePropertiesPanel: () => void;
79
+ runCanvasNode: (nodeId: string) => void;
80
+ toggleCanvasNodePin: (nodeId: string) => void;
81
+ editCanvasNodeOutput: (nodeId: string) => void;
82
+ clearCanvasNodePin: (nodeId: string) => void;
83
+ runWorkflowFromCanvas: () => void;
84
+ openLiveWorkflow: () => void;
85
+ openExecutionsPane: () => void;
86
+ copySelectedRunToLive: () => void;
87
+ isPanelCollapsed: boolean;
88
+ inspectorHeight: number;
89
+ startInspectorResize: (clientY: number) => void;
90
+ toggleInspectorPanel: () => void;
91
+ jsonEditorState: JsonEditorState | null;
92
+ closeJsonEditor: () => void;
93
+ saveJsonEditor: (value: string, binaryMaps?: PinBinaryMapsByItemIndex) => void;
94
+ workflowIsActive: boolean;
95
+ isWorkflowActivationPending: boolean;
96
+ workflowActivationAlertLines: ReadonlyArray<string> | null;
97
+ dismissWorkflowActivationAlert: () => void;
98
+ setWorkflowActive: (active: boolean) => void;
99
+ }>;
100
+
101
+ export function useWorkflowDetailController(
102
+ args: Readonly<{ workflowId: string; initialWorkflow?: WorkflowDto }>,
103
+ ): WorkflowDetailControllerResult {
104
+ const MIN_INSPECTOR_HEIGHT = 240;
105
+ const MAX_INSPECTOR_HEIGHT = 640;
106
+ const { workflowId, initialWorkflow } = args;
107
+ const router = useRouter();
108
+ const pathname = usePathname();
109
+ const searchParams = useSearchParams();
110
+ const urlLocation = useMemo(() => WorkflowDetailUrlCodec.parseSearchParams(searchParams), [searchParams]);
111
+ const navigateToLocation = useCallback(
112
+ (location: WorkflowDetailUrlLocation) => {
113
+ const href = WorkflowDetailUrlCodec.buildHref(pathname, searchParams, location);
114
+ router.replace(href);
115
+ },
116
+ [pathname, router, searchParams],
117
+ );
118
+ const queryClient = useQueryClient();
119
+ const workflowQuery = useWorkflowQuery(workflowId, initialWorkflow);
120
+ const setWorkflowActivationMutation = useSetWorkflowActivationMutation(workflowId);
121
+ const workflowCredentialHealthQuery = useWorkflowCredentialHealthQuery(workflowId);
122
+ const runsQuery = useWorkflowRunsQuery(workflowId);
123
+ const debuggerOverlayQuery = useWorkflowDebuggerOverlayQuery(workflowId);
124
+ const workflowDevBuildStateQuery = useWorkflowDevBuildStateQuery(workflowId);
125
+ const showRealtimeDisconnectedBadge = useWorkflowRealtimeShowDisconnectedBadge();
126
+ useWorkflowRealtimeSubscription(workflowId);
127
+
128
+ const workflowActivationErrorFormat = useMemo(() => new WorkflowActivationHttpErrorFormat(), []);
129
+ const [workflowActivationAlertLines, setWorkflowActivationAlertLines] = useState<ReadonlyArray<string> | null>(null);
130
+ const [error, setError] = useState<string | null>(null);
131
+ const [isRunRequestPending, setIsRunRequestPending] = useState(false);
132
+ const [pendingTriggerFetchSnapshot, setPendingTriggerFetchSnapshot] = useState<NodeExecutionSnapshot | null>(null);
133
+ const selectedRunId = urlLocation.selectedRunId;
134
+ const isRunsPaneVisible = urlLocation.isRunsPaneVisible;
135
+ const [activeLiveRunId, setActiveLiveRunId] = useState<string | null>(null);
136
+ const [pendingSelectedRun, setPendingSelectedRun] = useState<RunSummary | null>(null);
137
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
138
+ const [hasManuallySelectedNode, setHasManuallySelectedNode] = useState(false);
139
+ const [propertiesPanelNodeId, setPropertiesPanelNodeId] = useState<string | null>(null);
140
+ const [isPropertiesPanelOpen, setIsPropertiesPanelOpen] = useState(false);
141
+ const [selectedMode, setSelectedMode] = useState<InspectorMode>("output");
142
+ const [inspectorFormatByTab, setInspectorFormatByTab] = useState<Readonly<Record<InspectorTab, InspectorFormat>>>({
143
+ input: "json",
144
+ output: "json",
145
+ });
146
+ const [selectedInputPort, setSelectedInputPort] = useState<string | null>(null);
147
+ const [selectedOutputPort, setSelectedOutputPort] = useState<string | null>(null);
148
+ const [isPanelCollapsed, setIsPanelCollapsed] = useState(false);
149
+ const [inspectorHeight, setInspectorHeight] = useState(320);
150
+ const [isInspectorResizing, setIsInspectorResizing] = useState(false);
151
+ const [jsonEditorState, setJsonEditorState] = useState<JsonEditorState | null>(null);
152
+ const resizeStartYRef = useRef<number | null>(null);
153
+ const resizeStartHeightRef = useRef(320);
154
+ const previousInspectorSelectionRef = useRef("");
155
+ const previousInspectorHasErrorRef = useRef(false);
156
+ const runRequestInFlightRef = useRef(false);
157
+ const previousLiveWorkflowSignatureRef = useRef<string | null>(null);
158
+
159
+ const workflow = workflowQuery.data;
160
+ const workflowDevBuildState = workflowDevBuildStateQuery.data ?? {
161
+ state: "idle",
162
+ updatedAt: new Date(0).toISOString(),
163
+ };
164
+ const workflowDevBuildStateQueryKey = useMemo(() => ["workflow-dev-build-state", workflowId] as const, [workflowId]);
165
+ const liveWorkflowSignature = useMemo(
166
+ () => WorkflowDetailPresenter.createWorkflowStructureSignature(workflow),
167
+ [workflow],
168
+ );
169
+ const runs = runsQuery.data;
170
+ const selectedRunQuery = useRunQuery(selectedRunId);
171
+ const selectedRun = selectedRunQuery.data;
172
+ const activeLiveRunQuery = useRunQuery(activeLiveRunId, { pollWhileNonTerminalMs: 250 });
173
+ const activeLiveRun = activeLiveRunQuery.data;
174
+ const debuggerOverlay = debuggerOverlayQuery.data;
175
+ const viewContext = selectedRunId ? "historical-run" : "live-workflow";
176
+ const liveExecutionState = useMemo(() => {
177
+ const overlayCurrentState = debuggerOverlay?.currentState;
178
+ const baseLiveExecutionState = !activeLiveRunId
179
+ ? overlayCurrentState
180
+ : !activeLiveRun
181
+ ? ({
182
+ outputsByNode: {},
183
+ nodeSnapshotsByNodeId: {},
184
+ mutableState: overlayCurrentState?.mutableState,
185
+ connectionInvocations: overlayCurrentState?.connectionInvocations,
186
+ } satisfies NonNullable<NonNullable<typeof debuggerOverlay>["currentState"]>)
187
+ : ({
188
+ outputsByNode: activeLiveRun.outputsByNode,
189
+ nodeSnapshotsByNodeId: activeLiveRun.nodeSnapshotsByNodeId,
190
+ mutableState: overlayCurrentState?.mutableState ?? activeLiveRun.mutableState,
191
+ connectionInvocations: activeLiveRun.connectionInvocations ?? overlayCurrentState?.connectionInvocations,
192
+ } satisfies NonNullable<NonNullable<typeof debuggerOverlay>["currentState"]>);
193
+ const reconciledBaseLiveExecutionState = WorkflowDetailPresenter.reconcileCurrentStateWithWorkflow(
194
+ baseLiveExecutionState,
195
+ workflow,
196
+ );
197
+ if (!pendingTriggerFetchSnapshot || activeLiveRunId) {
198
+ return reconciledBaseLiveExecutionState;
199
+ }
200
+ if (!workflow?.nodes.some((node) => node.id === pendingTriggerFetchSnapshot.nodeId)) {
201
+ return reconciledBaseLiveExecutionState;
202
+ }
203
+ return {
204
+ ...(reconciledBaseLiveExecutionState ?? {
205
+ outputsByNode: {},
206
+ nodeSnapshotsByNodeId: {},
207
+ mutableState: overlayCurrentState?.mutableState,
208
+ connectionInvocations: overlayCurrentState?.connectionInvocations,
209
+ }),
210
+ nodeSnapshotsByNodeId: {
211
+ ...(reconciledBaseLiveExecutionState?.nodeSnapshotsByNodeId ?? {}),
212
+ [pendingTriggerFetchSnapshot.nodeId]: pendingTriggerFetchSnapshot,
213
+ },
214
+ } satisfies NonNullable<NonNullable<typeof debuggerOverlay>["currentState"]>;
215
+ }, [activeLiveRun, activeLiveRunId, debuggerOverlay, pendingTriggerFetchSnapshot, workflow]);
216
+ const displayedWorkflow = useMemo(
217
+ () => WorkflowDetailPresenter.resolveViewedWorkflow({ selectedRun, liveWorkflow: workflow }),
218
+ [selectedRun, workflow],
219
+ );
220
+ const currentExecutionState = useMemo(
221
+ () => (viewContext === "live-workflow" ? liveExecutionState : selectedRun),
222
+ [liveExecutionState, selectedRun, viewContext],
223
+ );
224
+ const normalizedConnectionInvocations = useMemo(
225
+ () => WorkflowDetailPresenter.normalizeConnectionInvocations(currentExecutionState?.connectionInvocations),
226
+ [currentExecutionState?.connectionInvocations],
227
+ );
228
+ const isActiveLiveRunPending = useMemo(
229
+ () =>
230
+ Boolean(
231
+ activeLiveRunId &&
232
+ (!activeLiveRun ||
233
+ activeLiveRun.status === "pending" ||
234
+ activeLiveRun.pending ||
235
+ Object.values(activeLiveRun.nodeSnapshotsByNodeId).some(
236
+ (snapshot) => snapshot.status === "queued" || snapshot.status === "running",
237
+ )),
238
+ ),
239
+ [activeLiveRun, activeLiveRunId],
240
+ );
241
+ const isRunning = isRunRequestPending || (viewContext === "live-workflow" && isActiveLiveRunPending);
242
+
243
+ const selectedPinnedOutput = useMemo(
244
+ () => WorkflowDetailPresenter.getPinnedOutput(currentExecutionState, selectedNodeId),
245
+ [currentExecutionState, selectedNodeId],
246
+ );
247
+ const pinnedNodeIds = useMemo(
248
+ () =>
249
+ new Set(
250
+ Object.keys(currentExecutionState?.mutableState?.nodesById ?? {}).filter((nodeId) =>
251
+ Boolean(currentExecutionState?.mutableState?.nodesById?.[nodeId]?.pinnedOutputsByPort?.main),
252
+ ),
253
+ ),
254
+ [currentExecutionState],
255
+ );
256
+ const displayedRuns = useMemo(() => {
257
+ if (!pendingSelectedRun) {
258
+ return runs;
259
+ }
260
+ if (!runs) {
261
+ return [pendingSelectedRun];
262
+ }
263
+ if (runs.some((run) => run.runId === pendingSelectedRun.runId)) {
264
+ return runs;
265
+ }
266
+ return [pendingSelectedRun, ...runs];
267
+ }, [pendingSelectedRun, runs]);
268
+
269
+ useEffect(() => {
270
+ if (pendingSelectedRun && runs?.some((run) => run.runId === pendingSelectedRun.runId)) {
271
+ setPendingSelectedRun(null);
272
+ }
273
+ }, [pendingSelectedRun, runs]);
274
+
275
+ useEffect(() => {
276
+ if (!selectedRunId) {
277
+ return;
278
+ }
279
+ if (displayedRuns === undefined) {
280
+ return;
281
+ }
282
+ if (displayedRuns.some((run) => run.runId === selectedRunId)) {
283
+ return;
284
+ }
285
+ navigateToLocation({
286
+ selectedRunId: null,
287
+ isRunsPaneVisible: true,
288
+ nodeId: urlLocation.nodeId,
289
+ });
290
+ }, [displayedRuns, navigateToLocation, selectedRunId, urlLocation.nodeId]);
291
+
292
+ useEffect(() => {
293
+ if (!selectedRunId) {
294
+ return;
295
+ }
296
+ if (selectedRunQuery.isLoading) {
297
+ return;
298
+ }
299
+ if (!selectedRunQuery.isError) {
300
+ return;
301
+ }
302
+ navigateToLocation({
303
+ selectedRunId: null,
304
+ isRunsPaneVisible: true,
305
+ nodeId: urlLocation.nodeId,
306
+ });
307
+ }, [navigateToLocation, selectedRunId, selectedRunQuery.isError, selectedRunQuery.isLoading, urlLocation.nodeId]);
308
+
309
+ useEffect(() => {
310
+ const nid = urlLocation.nodeId;
311
+ if (!nid || !displayedWorkflow) {
312
+ return;
313
+ }
314
+ if (displayedWorkflow.nodes.some((n) => n.id === nid)) {
315
+ return;
316
+ }
317
+ navigateToLocation({
318
+ selectedRunId: urlLocation.selectedRunId,
319
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
320
+ nodeId: null,
321
+ });
322
+ }, [
323
+ displayedWorkflow,
324
+ navigateToLocation,
325
+ urlLocation.isRunsPaneVisible,
326
+ urlLocation.nodeId,
327
+ urlLocation.selectedRunId,
328
+ ]);
329
+
330
+ useEffect(() => {
331
+ setHasManuallySelectedNode(false);
332
+ }, [selectedRunId]);
333
+
334
+ useEffect(() => {
335
+ const id = urlLocation.nodeId;
336
+ if (id !== null) {
337
+ setSelectedNodeId(id);
338
+ setHasManuallySelectedNode(true);
339
+ }
340
+ }, [urlLocation.nodeId]);
341
+
342
+ useEffect(() => {
343
+ if (urlLocation.nodeId === null) {
344
+ setHasManuallySelectedNode(false);
345
+ }
346
+ }, [urlLocation.nodeId]);
347
+
348
+ useEffect(() => {
349
+ setActiveLiveRunId(null);
350
+ setPendingSelectedRun(null);
351
+ setSelectedNodeId(null);
352
+ setHasManuallySelectedNode(false);
353
+ setPropertiesPanelNodeId(null);
354
+ setIsPropertiesPanelOpen(false);
355
+ setSelectedMode("output");
356
+ setInspectorFormatByTab({
357
+ input: "json",
358
+ output: "json",
359
+ });
360
+ setSelectedInputPort(null);
361
+ setSelectedOutputPort(null);
362
+ setIsPanelCollapsed(false);
363
+ setInspectorHeight(320);
364
+ setIsInspectorResizing(false);
365
+ setPendingTriggerFetchSnapshot(null);
366
+ setJsonEditorState(null);
367
+ resizeStartYRef.current = null;
368
+ resizeStartHeightRef.current = 320;
369
+ previousInspectorSelectionRef.current = "";
370
+ previousInspectorHasErrorRef.current = false;
371
+ }, [workflowId]);
372
+
373
+ useEffect(() => {
374
+ if (!workflow) {
375
+ return;
376
+ }
377
+ const previousSignature = previousLiveWorkflowSignatureRef.current;
378
+ previousLiveWorkflowSignatureRef.current = liveWorkflowSignature;
379
+ if (previousSignature === null || previousSignature === liveWorkflowSignature || selectedRunId) {
380
+ return;
381
+ }
382
+ queryClient.setQueryData(
383
+ WorkflowDetailPresenter.getWorkflowDebuggerOverlayQueryKey(workflowId),
384
+ (existing: typeof debuggerOverlay | undefined) => {
385
+ if (!existing) {
386
+ return existing;
387
+ }
388
+ return {
389
+ ...existing,
390
+ currentState:
391
+ WorkflowDetailPresenter.reconcileCurrentStateWithWorkflow(existing.currentState, workflow) ??
392
+ existing.currentState,
393
+ };
394
+ },
395
+ );
396
+ setActiveLiveRunId(null);
397
+ setPendingSelectedRun(null);
398
+ setPendingTriggerFetchSnapshot(null);
399
+ setJsonEditorState(null);
400
+ if (
401
+ selectedNodeId &&
402
+ !WorkflowDetailPresenter.inspectorSelectionAnchorsDisplayedWorkflow(
403
+ selectedNodeId,
404
+ workflow,
405
+ debuggerOverlay?.currentState.connectionInvocations,
406
+ )
407
+ ) {
408
+ setSelectedNodeId(null);
409
+ setHasManuallySelectedNode(false);
410
+ }
411
+ if (propertiesPanelNodeId && !workflow.nodes.some((node) => node.id === propertiesPanelNodeId)) {
412
+ setPropertiesPanelNodeId(null);
413
+ setIsPropertiesPanelOpen(false);
414
+ }
415
+ }, [
416
+ debuggerOverlay,
417
+ liveWorkflowSignature,
418
+ propertiesPanelNodeId,
419
+ queryClient,
420
+ selectedNodeId,
421
+ selectedRunId,
422
+ workflow,
423
+ workflowId,
424
+ ]);
425
+
426
+ useEffect(() => {
427
+ if (workflowDevBuildState.state !== "building" || !workflowDevBuildState.awaitingWorkflowRefreshAt) {
428
+ return;
429
+ }
430
+ if (workflowQuery.isFetching) {
431
+ return;
432
+ }
433
+ if (workflowQuery.isError) {
434
+ queryClient.setQueryData<WorkflowDevBuildState>(workflowDevBuildStateQueryKey, (existing) => {
435
+ if (!existing || existing.state !== "building") {
436
+ return existing;
437
+ }
438
+ return {
439
+ state: "idle",
440
+ updatedAt: new Date().toISOString(),
441
+ buildVersion: existing.buildVersion,
442
+ };
443
+ });
444
+ return;
445
+ }
446
+ const workflowRefreshRequestedAt = Date.parse(workflowDevBuildState.awaitingWorkflowRefreshAt);
447
+ if (!Number.isFinite(workflowRefreshRequestedAt) || workflowQuery.dataUpdatedAt < workflowRefreshRequestedAt) {
448
+ return;
449
+ }
450
+ queryClient.setQueryData<WorkflowDevBuildState>(workflowDevBuildStateQueryKey, (existing) => {
451
+ if (!existing || existing.state !== "building") {
452
+ return existing;
453
+ }
454
+ return {
455
+ state: "idle",
456
+ updatedAt: new Date().toISOString(),
457
+ buildVersion: existing.buildVersion,
458
+ };
459
+ });
460
+ }, [
461
+ queryClient,
462
+ workflowDevBuildState.awaitingWorkflowRefreshAt,
463
+ workflowDevBuildStateQueryKey,
464
+ workflowDevBuildState.state,
465
+ workflowQuery.dataUpdatedAt,
466
+ workflowQuery.isError,
467
+ workflowQuery.isFetching,
468
+ ]);
469
+
470
+ useEffect(() => {
471
+ if (!isInspectorResizing) return;
472
+ const handleMouseMove = (event: MouseEvent) => {
473
+ if (resizeStartYRef.current === null) return;
474
+ const nextHeight = resizeStartHeightRef.current + (resizeStartYRef.current - event.clientY);
475
+ setInspectorHeight(Math.max(MIN_INSPECTOR_HEIGHT, Math.min(MAX_INSPECTOR_HEIGHT, nextHeight)));
476
+ };
477
+ const handleMouseUp = () => {
478
+ setIsInspectorResizing(false);
479
+ resizeStartYRef.current = null;
480
+ };
481
+ window.addEventListener("mousemove", handleMouseMove);
482
+ window.addEventListener("mouseup", handleMouseUp);
483
+ return () => {
484
+ window.removeEventListener("mousemove", handleMouseMove);
485
+ window.removeEventListener("mouseup", handleMouseUp);
486
+ };
487
+ }, [isInspectorResizing]);
488
+
489
+ const executionNodes = useMemo(
490
+ () => WorkflowDetailPresenter.buildExecutionNodes(displayedWorkflow, currentExecutionState),
491
+ [currentExecutionState, displayedWorkflow],
492
+ );
493
+ const executionTreeData = useMemo(
494
+ () => WorkflowDetailPresenter.buildExecutionTreeData(executionNodes),
495
+ [executionNodes],
496
+ );
497
+ const executionTreeExpandedKeys = useMemo(
498
+ () => WorkflowDetailPresenter.collectExecutionTreeKeys(executionTreeData),
499
+ [executionTreeData],
500
+ );
501
+ const selectedExecutionTreeKey = useMemo(
502
+ () => WorkflowDetailPresenter.resolveExecutionTreeKeyForNodeId(executionNodes, selectedNodeId),
503
+ [executionNodes, selectedNodeId],
504
+ );
505
+ const credentialAttention = useMemo(
506
+ () =>
507
+ WorkflowDetailPresenter.resolveCredentialAttention({
508
+ workflow,
509
+ slots: workflowCredentialHealthQuery.data?.slots,
510
+ }),
511
+ [workflow, workflowCredentialHealthQuery.data?.slots],
512
+ );
513
+ const credentialAttentionTooltipByNodeId = useMemo(() => {
514
+ const map = new Map<string, string>();
515
+ for (const slot of workflowCredentialHealthQuery.data?.slots ?? []) {
516
+ if (slot.health.status !== "unbound") {
517
+ continue;
518
+ }
519
+ const nodeLabel = slot.nodeName ?? workflow?.nodes.find((n) => n.id === slot.nodeId)?.name ?? slot.nodeId;
520
+ const line = `${slot.requirement.label} (${slot.requirement.acceptedTypes.join(" · ")})`;
521
+ const existing = map.get(slot.nodeId);
522
+ map.set(slot.nodeId, existing ? `${existing}\n${line}` : `${nodeLabel}\n${line}`);
523
+ }
524
+ return map;
525
+ }, [workflow, workflowCredentialHealthQuery.data?.slots]);
526
+
527
+ const workflowNodeIdsWithBoundCredential = useMemo(() => {
528
+ const ids = new Set<string>();
529
+ for (const slot of workflowCredentialHealthQuery.data?.slots ?? []) {
530
+ if (slot.instance?.instanceId) {
531
+ ids.add(slot.nodeId);
532
+ }
533
+ }
534
+ return ids;
535
+ }, [workflowCredentialHealthQuery.data?.slots]);
536
+
537
+ const [pendingCredentialEditForNodeId, setPendingCredentialEditForNodeId] = useState<string | null>(null);
538
+
539
+ useEffect(() => {
540
+ if (!displayedWorkflow?.nodes.length) return;
541
+ if (
542
+ hasManuallySelectedNode &&
543
+ selectedNodeId &&
544
+ WorkflowDetailPresenter.inspectorSelectionAnchorsDisplayedWorkflow(
545
+ selectedNodeId,
546
+ displayedWorkflow,
547
+ currentExecutionState?.connectionInvocations,
548
+ )
549
+ ) {
550
+ return;
551
+ }
552
+ const orderedSnapshots = Object.values(currentExecutionState?.nodeSnapshotsByNodeId ?? {}).sort((left, right) => {
553
+ const leftTimestamp = WorkflowDetailPresenter.getSnapshotTimestamp(left) ?? "";
554
+ const rightTimestamp = WorkflowDetailPresenter.getSnapshotTimestamp(right) ?? "";
555
+ return rightTimestamp.localeCompare(leftTimestamp);
556
+ });
557
+ const nextFocusedNodeId =
558
+ orderedSnapshots.find((snapshot) => snapshot.status === "running")?.nodeId ??
559
+ orderedSnapshots.find((snapshot) => snapshot.status === "queued")?.nodeId ??
560
+ orderedSnapshots[0]?.nodeId ??
561
+ executionNodes[0]?.node.id ??
562
+ WorkflowDetailPresenter.getPreferredWorkflowNodeId(displayedWorkflow);
563
+ if (nextFocusedNodeId !== selectedNodeId) {
564
+ setSelectedNodeId(nextFocusedNodeId);
565
+ }
566
+ }, [
567
+ currentExecutionState,
568
+ displayedWorkflow,
569
+ executionNodes,
570
+ hasManuallySelectedNode,
571
+ normalizedConnectionInvocations,
572
+ selectedNodeId,
573
+ ]);
574
+
575
+ const selectedExecutionNode = useMemo(() => {
576
+ const direct = executionNodes.find((executionNode) => executionNode.node.id === selectedNodeId);
577
+ if (direct) {
578
+ return direct;
579
+ }
580
+ const byConnection = executionNodes
581
+ .filter((en) => en.workflowConnectionNodeId === selectedNodeId)
582
+ .sort((left, right) => {
583
+ const leftTs = WorkflowDetailPresenter.getSnapshotTimestamp(left.snapshot) ?? "";
584
+ const rightTs = WorkflowDetailPresenter.getSnapshotTimestamp(right.snapshot) ?? "";
585
+ return rightTs.localeCompare(leftTs);
586
+ });
587
+ return byConnection[0];
588
+ }, [executionNodes, selectedNodeId]);
589
+ const selectedNodeSnapshot = useMemo<NodeExecutionSnapshot | undefined>(() => {
590
+ if (!currentExecutionState || !selectedNodeId) return undefined;
591
+ return selectedExecutionNode?.snapshot ?? currentExecutionState.nodeSnapshotsByNodeId[selectedNodeId];
592
+ }, [currentExecutionState, selectedExecutionNode, selectedNodeId]);
593
+ const selectedWorkflowNode = useMemo(
594
+ () => selectedExecutionNode?.node ?? displayedWorkflow?.nodes.find((node) => node.id === selectedNodeId),
595
+ [displayedWorkflow, selectedExecutionNode, selectedNodeId],
596
+ );
597
+ const selectedPropertiesWorkflowNode = useMemo(
598
+ () =>
599
+ propertiesPanelNodeId ? displayedWorkflow?.nodes.find((node) => node.id === propertiesPanelNodeId) : undefined,
600
+ [displayedWorkflow, propertiesPanelNodeId],
601
+ );
602
+ const inputPortEntries = useMemo(
603
+ () => WorkflowDetailPresenter.sortPortEntries(selectedNodeSnapshot?.inputsByPort),
604
+ [selectedNodeSnapshot],
605
+ );
606
+ const outputPortEntries = useMemo(
607
+ () => WorkflowDetailPresenter.sortPortEntries(selectedNodeSnapshot?.outputs),
608
+ [selectedNodeSnapshot],
609
+ );
610
+ const visibleOutputPortEntries = useMemo(
611
+ () => WorkflowDetailPresenter.applyPinnedOutputToPortEntries(outputPortEntries, selectedPinnedOutput),
612
+ [outputPortEntries, selectedPinnedOutput],
613
+ );
614
+
615
+ useEffect(() => {
616
+ setSelectedInputPort((current) => WorkflowDetailPresenter.resolveSelectedPort(inputPortEntries, current));
617
+ }, [inputPortEntries]);
618
+
619
+ useEffect(() => {
620
+ setSelectedOutputPort((current) => WorkflowDetailPresenter.resolveSelectedPort(visibleOutputPortEntries, current));
621
+ }, [visibleOutputPortEntries]);
622
+
623
+ useEffect(() => {
624
+ const selectionKey = `${selectedRunId ?? ""}:${selectedNodeId ?? ""}`;
625
+ const nextHasError = Boolean(selectedNodeSnapshot?.error);
626
+ if (previousInspectorSelectionRef.current !== selectionKey && selectedMode !== "split") {
627
+ setSelectedMode(WorkflowDetailPresenter.getDefaultInspectorMode(selectedNodeSnapshot));
628
+ } else if (!previousInspectorHasErrorRef.current && nextHasError && selectedMode !== "split") {
629
+ setSelectedMode("output");
630
+ }
631
+ previousInspectorSelectionRef.current = selectionKey;
632
+ previousInspectorHasErrorRef.current = nextHasError;
633
+ }, [selectedMode, selectedNodeId, selectedNodeSnapshot, selectedRunId]);
634
+
635
+ const applyPendingRunResult = useCallback(
636
+ (
637
+ result: {
638
+ runId: string;
639
+ workflowId: string;
640
+ status: string;
641
+ startedAt?: string;
642
+ state: PersistedRunState | null;
643
+ },
644
+ options: Readonly<{ keepLiveWorkflow: boolean }>,
645
+ ) => {
646
+ if (result.state) {
647
+ queryClient.setQueryData(
648
+ WorkflowDetailPresenter.getWorkflowRunsQueryKey(result.workflowId),
649
+ (existing: ReadonlyArray<RunSummary> | undefined) =>
650
+ WorkflowDetailPresenter.mergeRunSummaryList(existing, WorkflowDetailPresenter.toRunSummary(result.state!)),
651
+ );
652
+ queryClient.setQueryData(WorkflowDetailPresenter.getRunQueryKey(result.runId), result.state);
653
+ }
654
+ if (options.keepLiveWorkflow) {
655
+ setActiveLiveRunId(result.runId);
656
+ navigateToLocation({
657
+ selectedRunId: null,
658
+ isRunsPaneVisible: false,
659
+ nodeId: null,
660
+ });
661
+ } else {
662
+ navigateToLocation({
663
+ selectedRunId: result.runId,
664
+ isRunsPaneVisible: true,
665
+ nodeId: null,
666
+ });
667
+ }
668
+ setPendingSelectedRun(
669
+ result.state
670
+ ? WorkflowDetailPresenter.toRunSummary(result.state)
671
+ : {
672
+ runId: result.runId,
673
+ workflowId: result.workflowId,
674
+ status: result.status,
675
+ startedAt: result.startedAt ?? new Date().toISOString(),
676
+ },
677
+ );
678
+ },
679
+ [navigateToLocation, queryClient],
680
+ );
681
+
682
+ const runExecution = useCallback(
683
+ (
684
+ request: RunWorkflowRequest = {},
685
+ options: Readonly<{ keepLiveWorkflow: boolean }> = { keepLiveWorkflow: false },
686
+ ) => {
687
+ if (runRequestInFlightRef.current || (options.keepLiveWorkflow && isActiveLiveRunPending)) {
688
+ return;
689
+ }
690
+ runRequestInFlightRef.current = true;
691
+ setIsRunRequestPending(true);
692
+ setError(null);
693
+ const nextRequest: RunWorkflowRequest = options.keepLiveWorkflow
694
+ ? {
695
+ ...request,
696
+ currentState: WorkflowDetailPresenter.createLiveRunCurrentState(request, currentExecutionState),
697
+ }
698
+ : request;
699
+ setPendingTriggerFetchSnapshot(
700
+ options.keepLiveWorkflow
701
+ ? (WorkflowDetailPresenter.createOptimisticTriggerFetchSnapshot(workflowId, workflow, nextRequest) ?? null)
702
+ : null,
703
+ );
704
+ void WorkflowDetailPresenter.runWorkflow(workflowId, workflow, nextRequest)
705
+ .then((result) => {
706
+ setPendingTriggerFetchSnapshot(null);
707
+ applyPendingRunResult(result, options);
708
+ })
709
+ .catch((cause: unknown) => {
710
+ setPendingTriggerFetchSnapshot(null);
711
+ setError(cause instanceof Error ? cause.message : String(cause));
712
+ })
713
+ .finally(() => {
714
+ runRequestInFlightRef.current = false;
715
+ setIsRunRequestPending(false);
716
+ });
717
+ },
718
+ [applyPendingRunResult, currentExecutionState, isActiveLiveRunPending, workflow, workflowId],
719
+ );
720
+
721
+ const onRun = useCallback(() => {
722
+ runExecution({ mode: "manual" }, { keepLiveWorkflow: true });
723
+ }, [runExecution]);
724
+
725
+ const replaceDebuggerOverlay = useCallback(
726
+ (nextCurrentState: NonNullable<typeof debuggerOverlay>["currentState"]) => {
727
+ setError(null);
728
+ return WorkflowDetailPresenter.replaceWorkflowDebuggerOverlay(workflowId, nextCurrentState).then((state) => {
729
+ queryClient.setQueryData(WorkflowDetailPresenter.getWorkflowDebuggerOverlayQueryKey(workflowId), state);
730
+ return state;
731
+ });
732
+ },
733
+ [queryClient, workflowId],
734
+ );
735
+
736
+ const createOverlayCurrentStateWithNodeState = useCallback(
737
+ (
738
+ nodeId: string,
739
+ values: Readonly<{
740
+ pinnedOutputsByPort?: NonNullable<
741
+ NonNullable<PersistedRunState["mutableState"]>["nodesById"][string]["pinnedOutputsByPort"]
742
+ >;
743
+ }>,
744
+ ) => {
745
+ const baseCurrentState = JSON.parse(
746
+ JSON.stringify(
747
+ currentExecutionState ?? {
748
+ outputsByNode: {},
749
+ nodeSnapshotsByNodeId: {},
750
+ mutableState: { nodesById: {} },
751
+ connectionInvocations: [],
752
+ },
753
+ ),
754
+ ) as NonNullable<typeof debuggerOverlay>["currentState"];
755
+ return {
756
+ ...baseCurrentState,
757
+ mutableState: {
758
+ nodesById: {
759
+ ...(baseCurrentState.mutableState?.nodesById ?? {}),
760
+ [nodeId]: {
761
+ ...(baseCurrentState.mutableState?.nodesById?.[nodeId] ?? {}),
762
+ ...values,
763
+ },
764
+ },
765
+ },
766
+ } satisfies NonNullable<typeof debuggerOverlay>["currentState"];
767
+ },
768
+ [currentExecutionState, debuggerOverlay],
769
+ );
770
+
771
+ const runNode = useCallback(
772
+ (nodeId: string) => {
773
+ if (viewContext !== "live-workflow") {
774
+ return;
775
+ }
776
+ setHasManuallySelectedNode(true);
777
+ setSelectedNodeId(nodeId);
778
+ navigateToLocation({
779
+ selectedRunId: urlLocation.selectedRunId,
780
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
781
+ nodeId,
782
+ });
783
+ runExecution(
784
+ {
785
+ stopAt: nodeId,
786
+ clearFromNodeId: nodeId,
787
+ mode: "manual",
788
+ },
789
+ { keepLiveWorkflow: true },
790
+ );
791
+ },
792
+ [navigateToLocation, runExecution, urlLocation.isRunsPaneVisible, urlLocation.selectedRunId, viewContext],
793
+ );
794
+
795
+ const onPinSelectedOutput = useCallback(() => {
796
+ if (!selectedNodeId || viewContext !== "live-workflow") {
797
+ return;
798
+ }
799
+ const baseItems = selectedPinnedOutput ?? selectedNodeSnapshot?.outputs?.main;
800
+ setJsonEditorState({
801
+ mode: "pin-output",
802
+ title: `Pin output for ${WorkflowDetailPresenter.getNodeDisplayName(selectedWorkflowNode, selectedNodeId)}`,
803
+ value: WorkflowDetailPresenter.toPinOutputEditorJson(baseItems),
804
+ workflowId,
805
+ nodeId: selectedNodeId,
806
+ binaryMapsByItemIndex: WorkflowDetailPresenter.extractBinaryMapsFromItems(baseItems),
807
+ });
808
+ }, [selectedNodeId, selectedNodeSnapshot, selectedPinnedOutput, selectedWorkflowNode, viewContext, workflowId]);
809
+
810
+ const openPinOutputEditor = useCallback(
811
+ (nodeId: string) => {
812
+ if (viewContext !== "live-workflow") {
813
+ return;
814
+ }
815
+ const workflowNode = displayedWorkflow?.nodes.find((node) => node.id === nodeId);
816
+ const snapshot = currentExecutionState?.nodeSnapshotsByNodeId?.[nodeId];
817
+ const pinnedOutput = WorkflowDetailPresenter.getPinnedOutput(currentExecutionState, nodeId);
818
+ const baseItems = pinnedOutput ?? snapshot?.outputs?.main;
819
+ setHasManuallySelectedNode(true);
820
+ setSelectedNodeId(nodeId);
821
+ navigateToLocation({
822
+ selectedRunId: urlLocation.selectedRunId,
823
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
824
+ nodeId,
825
+ });
826
+ setJsonEditorState({
827
+ mode: "pin-output",
828
+ title: `Edit output for ${WorkflowDetailPresenter.getNodeDisplayName(workflowNode, nodeId)}`,
829
+ value: WorkflowDetailPresenter.toPinOutputEditorJson(baseItems),
830
+ workflowId,
831
+ nodeId,
832
+ binaryMapsByItemIndex: WorkflowDetailPresenter.extractBinaryMapsFromItems(baseItems),
833
+ });
834
+ },
835
+ [
836
+ currentExecutionState,
837
+ displayedWorkflow,
838
+ navigateToLocation,
839
+ urlLocation.isRunsPaneVisible,
840
+ urlLocation.selectedRunId,
841
+ viewContext,
842
+ workflowId,
843
+ ],
844
+ );
845
+
846
+ const onClearPin = useCallback(() => {
847
+ if (!selectedNodeId || viewContext !== "live-workflow") {
848
+ return;
849
+ }
850
+ const nextCurrentState = createOverlayCurrentStateWithNodeState(selectedNodeId, {
851
+ pinnedOutputsByPort: undefined,
852
+ });
853
+ void replaceDebuggerOverlay(nextCurrentState).catch((cause: unknown) =>
854
+ setError(cause instanceof Error ? cause.message : String(cause)),
855
+ );
856
+ }, [createOverlayCurrentStateWithNodeState, replaceDebuggerOverlay, selectedNodeId, viewContext]);
857
+
858
+ const clearPinnedOutputForNode = useCallback(
859
+ (nodeId: string) => {
860
+ if (viewContext !== "live-workflow") {
861
+ return;
862
+ }
863
+ const nextCurrentState = createOverlayCurrentStateWithNodeState(nodeId, {
864
+ pinnedOutputsByPort: undefined,
865
+ });
866
+ setHasManuallySelectedNode(true);
867
+ setSelectedNodeId(nodeId);
868
+ navigateToLocation({
869
+ selectedRunId: urlLocation.selectedRunId,
870
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
871
+ nodeId,
872
+ });
873
+ void replaceDebuggerOverlay(nextCurrentState).catch((cause: unknown) =>
874
+ setError(cause instanceof Error ? cause.message : String(cause)),
875
+ );
876
+ },
877
+ [
878
+ createOverlayCurrentStateWithNodeState,
879
+ navigateToLocation,
880
+ replaceDebuggerOverlay,
881
+ urlLocation.isRunsPaneVisible,
882
+ urlLocation.selectedRunId,
883
+ viewContext,
884
+ ],
885
+ );
886
+
887
+ const togglePinnedOutputForNode = useCallback(
888
+ (nodeId: string) => {
889
+ if (viewContext !== "live-workflow") {
890
+ return;
891
+ }
892
+ const pinnedOutput = WorkflowDetailPresenter.getPinnedOutput(currentExecutionState, nodeId);
893
+ setHasManuallySelectedNode(true);
894
+ setSelectedNodeId(nodeId);
895
+ navigateToLocation({
896
+ selectedRunId: urlLocation.selectedRunId,
897
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
898
+ nodeId,
899
+ });
900
+ if (pinnedOutput) {
901
+ const nextCurrentState = createOverlayCurrentStateWithNodeState(nodeId, {
902
+ pinnedOutputsByPort: undefined,
903
+ });
904
+ void replaceDebuggerOverlay(nextCurrentState).catch((cause: unknown) =>
905
+ setError(cause instanceof Error ? cause.message : String(cause)),
906
+ );
907
+ return;
908
+ }
909
+ const outputToPin = currentExecutionState?.nodeSnapshotsByNodeId?.[nodeId]?.outputs?.main;
910
+ if (!outputToPin) {
911
+ return;
912
+ }
913
+ const nextCurrentState = createOverlayCurrentStateWithNodeState(nodeId, {
914
+ pinnedOutputsByPort: {
915
+ main: JSON.parse(JSON.stringify(outputToPin)) as Items,
916
+ },
917
+ });
918
+ void replaceDebuggerOverlay(nextCurrentState).catch((cause: unknown) =>
919
+ setError(cause instanceof Error ? cause.message : String(cause)),
920
+ );
921
+ },
922
+ [
923
+ createOverlayCurrentStateWithNodeState,
924
+ currentExecutionState,
925
+ navigateToLocation,
926
+ replaceDebuggerOverlay,
927
+ urlLocation.isRunsPaneVisible,
928
+ urlLocation.selectedRunId,
929
+ viewContext,
930
+ ],
931
+ );
932
+
933
+ const onCopyToDebugger = useCallback(() => {
934
+ if (!selectedRun) {
935
+ return;
936
+ }
937
+ setError(null);
938
+ void WorkflowDetailPresenter.copyRunToDebuggerOverlay(workflowId, selectedRun.runId)
939
+ .then((state) => {
940
+ queryClient.setQueryData(WorkflowDetailPresenter.getWorkflowDebuggerOverlayQueryKey(workflowId), state);
941
+ setActiveLiveRunId(null);
942
+ navigateToLocation({
943
+ selectedRunId: null,
944
+ isRunsPaneVisible: false,
945
+ nodeId: null,
946
+ });
947
+ })
948
+ .catch((cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)));
949
+ }, [navigateToLocation, queryClient, selectedRun, workflowId]);
950
+
951
+ const onSelectRun = useCallback(
952
+ (runId: string) => {
953
+ navigateToLocation({
954
+ selectedRunId: runId,
955
+ isRunsPaneVisible: true,
956
+ nodeId: null,
957
+ });
958
+ },
959
+ [navigateToLocation],
960
+ );
961
+
962
+ const onSelectLiveWorkflow = useCallback(() => {
963
+ navigateToLocation({
964
+ selectedRunId: null,
965
+ isRunsPaneVisible: false,
966
+ nodeId: null,
967
+ });
968
+ }, [navigateToLocation]);
969
+
970
+ const onOpenExecutionsPane = useCallback(() => {
971
+ navigateToLocation({
972
+ selectedRunId: urlLocation.selectedRunId,
973
+ isRunsPaneVisible: true,
974
+ nodeId: urlLocation.nodeId,
975
+ });
976
+ }, [navigateToLocation, urlLocation.nodeId, urlLocation.selectedRunId]);
977
+
978
+ const persistWorkflowSnapshotUpdate = useCallback(
979
+ (runId: string, value: string) => {
980
+ void WorkflowDetailPresenter.updateWorkflowSnapshot(runId, WorkflowDetailPresenter.parseWorkflowSnapshot(value))
981
+ .then((state) => {
982
+ queryClient.setQueryData(WorkflowDetailPresenter.getRunQueryKey(state.runId), state);
983
+ setJsonEditorState(null);
984
+ })
985
+ .catch((cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)));
986
+ },
987
+ [queryClient],
988
+ );
989
+
990
+ const saveJsonEditor = useCallback(
991
+ (value: string, binaryMaps?: PinBinaryMapsByItemIndex) => {
992
+ if (!jsonEditorState) {
993
+ return;
994
+ }
995
+ if (jsonEditorState.mode === "workflow-snapshot") {
996
+ if (!selectedRunId) return;
997
+ persistWorkflowSnapshotUpdate(selectedRunId, value);
998
+ return;
999
+ }
1000
+ if (!selectedRunId || !selectedNodeId) {
1001
+ if (!selectedNodeId || viewContext !== "live-workflow") {
1002
+ return;
1003
+ }
1004
+ }
1005
+ if (jsonEditorState.mode === "pin-output") {
1006
+ const nodeIdForPin = jsonEditorState.nodeId;
1007
+ const pinnedItems =
1008
+ binaryMaps !== undefined
1009
+ ? WorkflowDetailPresenter.mergePinOutputJsonWithBinaryMaps(value, binaryMaps)
1010
+ : WorkflowDetailPresenter.parseEditableItems(value);
1011
+ const nextCurrentState = createOverlayCurrentStateWithNodeState(nodeIdForPin, {
1012
+ pinnedOutputsByPort: { main: pinnedItems },
1013
+ });
1014
+ void replaceDebuggerOverlay(nextCurrentState)
1015
+ .then(() => {
1016
+ setJsonEditorState(null);
1017
+ })
1018
+ .catch((cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)));
1019
+ return;
1020
+ }
1021
+ if (!selectedRunId) {
1022
+ return;
1023
+ }
1024
+ persistWorkflowSnapshotUpdate(selectedRunId, value);
1025
+ },
1026
+ [
1027
+ applyPendingRunResult,
1028
+ createOverlayCurrentStateWithNodeState,
1029
+ jsonEditorState,
1030
+ persistWorkflowSnapshotUpdate,
1031
+ queryClient,
1032
+ replaceDebuggerOverlay,
1033
+ selectedNodeId,
1034
+ selectedRunId,
1035
+ viewContext,
1036
+ workflow,
1037
+ workflowId,
1038
+ ],
1039
+ );
1040
+
1041
+ const workflowError = workflowQuery.error instanceof Error ? workflowQuery.error.message : null;
1042
+ const runsError = runsQuery.error instanceof Error ? runsQuery.error.message : null;
1043
+ const inspectorLoadError =
1044
+ viewContext === "historical-run"
1045
+ ? selectedRunQuery.error instanceof Error
1046
+ ? selectedRunQuery.error.message
1047
+ : null
1048
+ : debuggerOverlayQuery.error instanceof Error
1049
+ ? debuggerOverlayQuery.error.message
1050
+ : null;
1051
+ const selectedInputItems = useMemo(
1052
+ () => inputPortEntries.find(([portName]) => portName === selectedInputPort)?.[1],
1053
+ [inputPortEntries, selectedInputPort],
1054
+ );
1055
+ const selectedOutputItems = useMemo(
1056
+ () => visibleOutputPortEntries.find(([portName]) => portName === selectedOutputPort)?.[1],
1057
+ [selectedOutputPort, visibleOutputPortEntries],
1058
+ );
1059
+ const selectedNodeError = selectedNodeSnapshot?.error;
1060
+ const inputAttachments = useMemo(
1061
+ () => WorkflowDetailPresenter.toAttachmentModels(selectedInputItems, workflowId, viewContext),
1062
+ [selectedInputItems, viewContext, workflowId],
1063
+ );
1064
+ const outputAttachments = useMemo(
1065
+ () =>
1066
+ WorkflowDetailPresenter.toAttachmentModels(
1067
+ selectedNodeError ? undefined : selectedOutputItems,
1068
+ workflowId,
1069
+ viewContext,
1070
+ ),
1071
+ [selectedNodeError, selectedOutputItems, viewContext, workflowId],
1072
+ );
1073
+
1074
+ useEffect(() => {
1075
+ setInspectorFormatByTab((current) => {
1076
+ const nextInputFormat = current.input === "binary" && inputAttachments.length === 0 ? "json" : current.input;
1077
+ const nextOutputFormat = current.output === "binary" && outputAttachments.length === 0 ? "json" : current.output;
1078
+ if (nextInputFormat === current.input && nextOutputFormat === current.output) {
1079
+ return current;
1080
+ }
1081
+ return {
1082
+ input: nextInputFormat,
1083
+ output: nextOutputFormat,
1084
+ };
1085
+ });
1086
+ }, [inputAttachments.length, outputAttachments.length]);
1087
+
1088
+ const inputPane = {
1089
+ tab: "input" as const,
1090
+ format: inspectorFormatByTab.input,
1091
+ selectedPort: selectedInputPort,
1092
+ portEntries: inputPortEntries,
1093
+ value: WorkflowDetailPresenter.toJsonValue(selectedInputItems),
1094
+ attachments: inputAttachments,
1095
+ emptyLabel: "No input captured yet.",
1096
+ showsError: false,
1097
+ };
1098
+ const outputPane = {
1099
+ tab: "output" as const,
1100
+ format: inspectorFormatByTab.output,
1101
+ selectedPort: selectedOutputPort,
1102
+ portEntries: selectedNodeError ? [] : visibleOutputPortEntries,
1103
+ value: selectedNodeError ?? WorkflowDetailPresenter.toJsonValue(selectedOutputItems),
1104
+ attachments: outputAttachments,
1105
+ emptyLabel: selectedNodeError ? "No error for this node." : "No output captured yet.",
1106
+ showsError: Boolean(selectedNodeError),
1107
+ };
1108
+
1109
+ const selectNode = useCallback(
1110
+ (nodeId: string) => {
1111
+ setHasManuallySelectedNode(true);
1112
+ setSelectedNodeId(nodeId);
1113
+ navigateToLocation({
1114
+ selectedRunId: urlLocation.selectedRunId,
1115
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
1116
+ nodeId,
1117
+ });
1118
+ },
1119
+ [navigateToLocation, urlLocation.isRunsPaneVisible, urlLocation.selectedRunId],
1120
+ );
1121
+
1122
+ const selectCanvasNode = useCallback(
1123
+ (nodeId: string) => {
1124
+ setHasManuallySelectedNode(true);
1125
+ const resolved = WorkflowDetailPresenter.resolveInspectorNodeIdForCanvasPick(
1126
+ nodeId,
1127
+ displayedWorkflow,
1128
+ currentExecutionState?.nodeSnapshotsByNodeId,
1129
+ normalizedConnectionInvocations,
1130
+ );
1131
+ setSelectedNodeId(resolved);
1132
+ navigateToLocation({
1133
+ selectedRunId: urlLocation.selectedRunId,
1134
+ isRunsPaneVisible: urlLocation.isRunsPaneVisible,
1135
+ nodeId: resolved,
1136
+ });
1137
+ },
1138
+ [
1139
+ currentExecutionState?.nodeSnapshotsByNodeId,
1140
+ displayedWorkflow,
1141
+ navigateToLocation,
1142
+ normalizedConnectionInvocations,
1143
+ urlLocation.isRunsPaneVisible,
1144
+ urlLocation.selectedRunId,
1145
+ ],
1146
+ );
1147
+
1148
+ const openPropertiesPanelForNode = useCallback((nodeId: string) => {
1149
+ setPropertiesPanelNodeId(nodeId);
1150
+ setIsPropertiesPanelOpen(true);
1151
+ }, []);
1152
+
1153
+ const requestOpenCredentialEditForNode = useCallback((nodeId: string) => {
1154
+ setPendingCredentialEditForNodeId(nodeId);
1155
+ setPropertiesPanelNodeId(nodeId);
1156
+ setIsPropertiesPanelOpen(true);
1157
+ }, []);
1158
+
1159
+ const consumePendingCredentialEditRequest = useCallback(() => {
1160
+ setPendingCredentialEditForNodeId(null);
1161
+ }, []);
1162
+
1163
+ const closePropertiesPanel = useCallback(() => {
1164
+ setIsPropertiesPanelOpen(false);
1165
+ setPropertiesPanelNodeId(null);
1166
+ setPendingCredentialEditForNodeId(null);
1167
+ }, []);
1168
+
1169
+ return {
1170
+ displayedWorkflow,
1171
+ displayedNodeSnapshotsByNodeId: currentExecutionState?.nodeSnapshotsByNodeId ?? {},
1172
+ displayedConnectionInvocations: normalizedConnectionInvocations,
1173
+ pinnedNodeIds,
1174
+ isLiveWorkflowView: viewContext === "live-workflow",
1175
+ isRunsPaneVisible,
1176
+ isRunning,
1177
+ workflowDevBuildState,
1178
+ showRealtimeDisconnectedBadge,
1179
+ canCopySelectedRunToLive: viewContext === "historical-run" && Boolean(selectedRun),
1180
+ credentialAttentionNodeIds: credentialAttention.attentionNodeIds,
1181
+ credentialAttentionSummaryLines: credentialAttention.summaryLines,
1182
+ credentialAttentionTooltipByNodeId,
1183
+ workflowNodeIdsWithBoundCredential,
1184
+ selectedRun,
1185
+ sidebarModel: {
1186
+ workflowId,
1187
+ displayedWorkflow,
1188
+ workflow,
1189
+ workflowError,
1190
+ error,
1191
+ displayedRuns,
1192
+ runsError,
1193
+ selectedRunId,
1194
+ selectedRun,
1195
+ },
1196
+ sidebarFormatting: {
1197
+ formatDateTime: WorkflowDetailPresenter.formatDateTime,
1198
+ formatRunListWhen: WorkflowDetailPresenter.formatRunListWhen,
1199
+ formatRunListDurationLine: WorkflowDetailPresenter.formatRunListDurationLine,
1200
+ getExecutionModeLabel: WorkflowDetailPresenter.getExecutionModeLabel,
1201
+ },
1202
+ sidebarActions: {
1203
+ onSelectRun,
1204
+ },
1205
+ inspectorModel: {
1206
+ workflowId,
1207
+ viewContext,
1208
+ selectedRunId,
1209
+ isLoading: viewContext === "historical-run" ? selectedRunQuery.isLoading : debuggerOverlayQuery.isLoading,
1210
+ loadError: inspectorLoadError,
1211
+ selectedRun,
1212
+ selectedNodeId,
1213
+ selectedNodeSnapshot,
1214
+ selectedWorkflowNode,
1215
+ selectedPinnedOutput,
1216
+ selectedNodeError,
1217
+ selectedMode,
1218
+ inputPane,
1219
+ outputPane,
1220
+ executionTreeData,
1221
+ executionTreeExpandedKeys,
1222
+ selectedExecutionTreeKey,
1223
+ nodeActions: {
1224
+ viewContext,
1225
+ isRunning,
1226
+ canEditOutput: viewContext === "live-workflow" && Boolean(selectedNodeId),
1227
+ canClearPinnedOutput: viewContext === "live-workflow" && Boolean(selectedNodeId && selectedPinnedOutput),
1228
+ },
1229
+ },
1230
+ inspectorFormatting: {
1231
+ formatDateTime: WorkflowDetailPresenter.formatDateTime,
1232
+ formatDurationLabel: WorkflowDetailPresenter.formatDurationLabel,
1233
+ getNodeDisplayName: WorkflowDetailPresenter.getNodeDisplayName,
1234
+ getSnapshotTimestamp: WorkflowDetailPresenter.getSnapshotTimestamp,
1235
+ getErrorHeadline: WorkflowDetailPresenter.getErrorHeadline,
1236
+ getErrorStack: WorkflowDetailPresenter.getErrorStack,
1237
+ getErrorClipboardText: WorkflowDetailPresenter.getErrorClipboardText,
1238
+ },
1239
+ inspectorActions: {
1240
+ onSelectNode: selectNode,
1241
+ onEditSelectedOutput: onPinSelectedOutput,
1242
+ onClearPinnedOutput: onClearPin,
1243
+ onSelectMode: setSelectedMode,
1244
+ onSelectFormat: (tab, format) =>
1245
+ setInspectorFormatByTab((current) => ({
1246
+ ...current,
1247
+ [tab]: format,
1248
+ })),
1249
+ onSelectInputPort: setSelectedInputPort,
1250
+ onSelectOutputPort: setSelectedOutputPort,
1251
+ },
1252
+ selectedNodeId,
1253
+ propertiesPanelNodeId,
1254
+ isPropertiesPanelOpen,
1255
+ selectedPropertiesWorkflowNode,
1256
+ selectCanvasNode,
1257
+ openPropertiesPanelForNode,
1258
+ requestOpenCredentialEditForNode,
1259
+ pendingCredentialEditForNodeId,
1260
+ consumePendingCredentialEditRequest,
1261
+ closePropertiesPanel,
1262
+ runCanvasNode: runNode,
1263
+ toggleCanvasNodePin: togglePinnedOutputForNode,
1264
+ editCanvasNodeOutput: openPinOutputEditor,
1265
+ clearCanvasNodePin: clearPinnedOutputForNode,
1266
+ runWorkflowFromCanvas: onRun,
1267
+ openLiveWorkflow: onSelectLiveWorkflow,
1268
+ openExecutionsPane: onOpenExecutionsPane,
1269
+ copySelectedRunToLive: onCopyToDebugger,
1270
+ isPanelCollapsed,
1271
+ inspectorHeight,
1272
+ startInspectorResize: (clientY) => {
1273
+ if (isPanelCollapsed) return;
1274
+ resizeStartYRef.current = clientY;
1275
+ resizeStartHeightRef.current = inspectorHeight;
1276
+ setIsInspectorResizing(true);
1277
+ },
1278
+ toggleInspectorPanel: () => setIsPanelCollapsed((value) => !value),
1279
+ jsonEditorState,
1280
+ closeJsonEditor: () => setJsonEditorState(null),
1281
+ saveJsonEditor,
1282
+ workflowIsActive: workflow?.active ?? false,
1283
+ isWorkflowActivationPending: setWorkflowActivationMutation.isPending,
1284
+ workflowActivationAlertLines,
1285
+ dismissWorkflowActivationAlert: () => {
1286
+ setWorkflowActivationAlertLines(null);
1287
+ },
1288
+ setWorkflowActive: (active: boolean) => {
1289
+ setWorkflowActivationAlertLines(null);
1290
+ setWorkflowActivationMutation.mutate(active, {
1291
+ onSuccess: () => {
1292
+ setWorkflowActivationAlertLines(null);
1293
+ },
1294
+ onError: (mutationError) => {
1295
+ setWorkflowActivationAlertLines(workflowActivationErrorFormat.extractMessages(mutationError));
1296
+ },
1297
+ });
1298
+ },
1299
+ };
1300
+ }