@xdelivered/emberflow 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { ChevronRightIcon, CodeIcon, PinIcon, PlayIcon } from 'lucide-react';
|
|
3
|
+
import { BranchRulesEditor } from './BranchRulesEditor';
|
|
4
|
+
import { ExecutionPager } from './ExecutionPager';
|
|
5
|
+
import { NodeRunModal } from './NodeRunModal';
|
|
6
|
+
import { Json } from './Json';
|
|
7
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
8
|
+
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
9
|
+
import { Badge } from '@/components/ui/badge';
|
|
10
|
+
import { Button } from '@/components/ui/button';
|
|
11
|
+
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
|
|
12
|
+
import { Combobox } from '@/components/ui/combobox';
|
|
13
|
+
import type { ComboboxOption } from '@/components/ui/combobox';
|
|
14
|
+
import { Input } from '@/components/ui/input';
|
|
15
|
+
import { useBuilderStore } from '../store/builderStore';
|
|
16
|
+
import { isMountablePath } from '../../server/pathGuard';
|
|
17
|
+
import type { AuthPolicy, FieldDefinition, HttpTrigger, LogLine, WorkflowNode } from '../engine';
|
|
18
|
+
import { keyValueRows, simpleNodeDescription } from '@/lib/registerLens';
|
|
19
|
+
import { cn } from '@/lib/utils';
|
|
20
|
+
|
|
21
|
+
// Trace-kind presentation, mirrored from the runbook badge hover card so the
|
|
22
|
+
// Inspector speaks the same language (SQL/HTTP/LLM · query/endpoint/model).
|
|
23
|
+
const TRACE_KIND_LABEL: Record<string, string> = { db: 'SQL', http: 'HTTP', llm: 'LLM', compute: 'FN' };
|
|
24
|
+
const TRACE_KIND_COLOR: Record<string, string> = { db: '#8fb8d8', http: '#c9a6e8', llm: '#d8c88f', compute: '#9aa0a6' };
|
|
25
|
+
const TRACE_KIND_DETAIL_LABEL: Record<string, string> = { db: 'query', http: 'endpoint', llm: 'model', compute: 'detail' };
|
|
26
|
+
|
|
27
|
+
function statusText(status: string) {
|
|
28
|
+
if (status === 'succeeded') return 'text-success';
|
|
29
|
+
if (status === 'failed') return 'text-destructive';
|
|
30
|
+
if (status === 'running') return 'text-highlight';
|
|
31
|
+
return 'text-muted-foreground';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function statusDotClass(status: string) {
|
|
35
|
+
if (status === 'succeeded') return 'bg-success';
|
|
36
|
+
if (status === 'failed') return 'bg-destructive';
|
|
37
|
+
if (status === 'running') return 'bg-highlight animate-pulse';
|
|
38
|
+
return 'bg-muted-foreground';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Simple outcome line: the last info-level log for this node in the
|
|
43
|
+
* current run — same rule the runbook projection uses for step outcomes
|
|
44
|
+
* (see lastInfoOutcome in runbookProjection.ts). Kept local here since that
|
|
45
|
+
* helper isn't exported; duplicating a four-line loop beats reaching across
|
|
46
|
+
* modules for it.
|
|
47
|
+
*/
|
|
48
|
+
function lastInfoOutcome(logs: LogLine[], nodeId: string): string {
|
|
49
|
+
for (let i = logs.length - 1; i >= 0; i--) {
|
|
50
|
+
const line = logs[i];
|
|
51
|
+
if (line.nodeId === nodeId && line.level === 'info') return line.message;
|
|
52
|
+
}
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Simple register key-value grid for a node's input/output payload —
|
|
58
|
+
* two-column rows (mono key · plain value), with complex (object/array)
|
|
59
|
+
* values collapsed behind a toggle button that expands an inline <Json>
|
|
60
|
+
* block. The destructive error pane is unchanged and never folds.
|
|
61
|
+
*/
|
|
62
|
+
function KeyValueGrid({
|
|
63
|
+
value,
|
|
64
|
+
error,
|
|
65
|
+
emptyLabel,
|
|
66
|
+
}: {
|
|
67
|
+
value: Record<string, unknown> | undefined;
|
|
68
|
+
error?: string;
|
|
69
|
+
emptyLabel: string;
|
|
70
|
+
}) {
|
|
71
|
+
const rows = keyValueRows(value);
|
|
72
|
+
const [open, setOpen] = useState<Record<string, boolean>>({});
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setOpen({});
|
|
76
|
+
}, [value]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
{rows.length === 0 && (
|
|
81
|
+
<div className="text-[12px] text-muted-foreground/60">{emptyLabel}</div>
|
|
82
|
+
)}
|
|
83
|
+
{rows.map((row) => (
|
|
84
|
+
<div className="mb-1.5" key={row.key}>
|
|
85
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
86
|
+
<span className="shrink-0 font-mono text-[10.5px] text-muted-foreground">{row.key}</span>
|
|
87
|
+
{row.complex ? (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => setOpen((o) => ({ ...o, [row.key]: !o[row.key] }))}
|
|
91
|
+
aria-expanded={!!open[row.key]}
|
|
92
|
+
className="min-w-0 truncate text-[12px] text-foreground underline decoration-dotted underline-offset-2 hover:text-highlight"
|
|
93
|
+
>
|
|
94
|
+
{row.display}
|
|
95
|
+
</button>
|
|
96
|
+
) : (
|
|
97
|
+
<span className="min-w-0 truncate text-[12px] text-foreground" title={row.display}>
|
|
98
|
+
{row.display}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{row.complex && open[row.key] && (
|
|
103
|
+
<div className="mt-1">
|
|
104
|
+
<Json value={(value as Record<string, unknown>)[row.key]} maxHeight={220} />
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
{error && (
|
|
110
|
+
<pre className="mt-2 overflow-x-auto rounded-md border border-destructive/50 bg-background p-2 font-mono text-[11.5px] whitespace-pre-wrap text-destructive-foreground">
|
|
111
|
+
{error}
|
|
112
|
+
</pre>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The declared shape of a node's input or output — field names (with types) it
|
|
120
|
+
* will carry, before any run. Regular nodes read their schema; dynamic nodes
|
|
121
|
+
* (Input) declare no schema and instead carry their emitted field names in
|
|
122
|
+
* config.fields, which are the node's *output* shape. Returns [] when there is
|
|
123
|
+
* genuinely nothing to describe, so the caller can drop the section entirely
|
|
124
|
+
* rather than print a "nothing yet" placeholder.
|
|
125
|
+
*/
|
|
126
|
+
function shapeFields(
|
|
127
|
+
node: WorkflowNode,
|
|
128
|
+
definition: { inputSchema?: { fields: FieldDefinition[] }; outputSchema?: { fields: FieldDefinition[] } } | undefined,
|
|
129
|
+
kind: 'input' | 'output',
|
|
130
|
+
): { name: string; type?: string }[] {
|
|
131
|
+
const declared = (kind === 'input' ? definition?.inputSchema : definition?.outputSchema)?.fields ?? [];
|
|
132
|
+
if (declared.length) return declared.map((f) => ({ name: f.name, type: f.type }));
|
|
133
|
+
if (kind === 'output' && Array.isArray(node.config?.fields)) {
|
|
134
|
+
return (node.config.fields as Array<{ name?: unknown; type?: unknown }>)
|
|
135
|
+
.filter((f) => typeof f?.name === 'string' && f.name)
|
|
136
|
+
.map((f) => ({ name: String(f.name), type: typeof f.type === 'string' ? f.type : undefined }));
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The expected shape as a quiet field-name·type list — shown before a run in
|
|
142
|
+
* place of a "nothing yet" placeholder, so the user sees what will flow. */
|
|
143
|
+
function ShapeList({ fields }: { fields: { name: string; type?: string }[] }) {
|
|
144
|
+
return (
|
|
145
|
+
<div>
|
|
146
|
+
{fields.map((f) => (
|
|
147
|
+
<div className="mb-1 flex items-baseline justify-between gap-3" key={f.name}>
|
|
148
|
+
<span className="font-mono text-[10.5px] text-muted-foreground">{f.name}</span>
|
|
149
|
+
{f.type && <span className="shrink-0 text-[10px] text-muted-foreground/50">{f.type}</span>}
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function FieldTag({ field }: { field: FieldDefinition }) {
|
|
157
|
+
return (
|
|
158
|
+
<span className="font-mono text-[10px] text-muted-foreground/70">
|
|
159
|
+
{field.type}
|
|
160
|
+
{field.type === 'enum' && field.enumValues ? `: ${field.enumValues.join(' | ')}` : ''}
|
|
161
|
+
</span>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function FieldName({ field }: { field: FieldDefinition }) {
|
|
166
|
+
return (
|
|
167
|
+
<span className="font-mono text-[11.5px] text-foreground/90">
|
|
168
|
+
{field.name}
|
|
169
|
+
{field.required && <span className="ml-0.5 text-destructive">*</span>}
|
|
170
|
+
</span>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ValuePreview({ value }: { value: unknown }) {
|
|
175
|
+
if (value === undefined) return <span className="text-[11px] text-muted-foreground/60">—</span>;
|
|
176
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
177
|
+
return (
|
|
178
|
+
<code
|
|
179
|
+
className="inline-block max-w-full truncate rounded-sm bg-secondary/60 px-1.5 font-mono text-[11px] text-muted-foreground"
|
|
180
|
+
title={text}
|
|
181
|
+
>
|
|
182
|
+
{text}
|
|
183
|
+
</code>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function RunValue({ label, value }: { label: string; value: unknown }) {
|
|
188
|
+
return (
|
|
189
|
+
<div className="mt-1 flex items-baseline gap-2">
|
|
190
|
+
<span className="shrink-0 text-[9.5px] font-medium uppercase tracking-widest text-muted-foreground/60">
|
|
191
|
+
{label}
|
|
192
|
+
</span>
|
|
193
|
+
<ValuePreview value={value} />
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Declared output fields for a node, with values from a given output payload (last run or a selected iteration record). */
|
|
199
|
+
function OutputFieldsList({
|
|
200
|
+
fields,
|
|
201
|
+
outputValue,
|
|
202
|
+
error,
|
|
203
|
+
}: {
|
|
204
|
+
fields: FieldDefinition[];
|
|
205
|
+
outputValue: Record<string, unknown> | undefined;
|
|
206
|
+
error?: string;
|
|
207
|
+
}) {
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
{fields.map((f) => (
|
|
211
|
+
<div className="mb-3" key={f.name}>
|
|
212
|
+
<div className="mb-1 flex items-baseline justify-between">
|
|
213
|
+
<FieldName field={f} />
|
|
214
|
+
<FieldTag field={f} />
|
|
215
|
+
</div>
|
|
216
|
+
{outputValue !== undefined && <RunValue label="last run" value={outputValue?.[f.name]} />}
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
{fields.length === 0 && (
|
|
220
|
+
<div className="text-[12px] text-muted-foreground/60">No declared outputs.</div>
|
|
221
|
+
)}
|
|
222
|
+
{error && (
|
|
223
|
+
<pre className="mt-2 overflow-x-auto rounded-md border border-destructive/50 bg-background p-2 font-mono text-[11.5px] whitespace-pre-wrap text-destructive-foreground">
|
|
224
|
+
{error}
|
|
225
|
+
</pre>
|
|
226
|
+
)}
|
|
227
|
+
</>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
232
|
+
return (
|
|
233
|
+
<h3 className="mb-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
|
|
234
|
+
{children}
|
|
235
|
+
</h3>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isSecretRef(value: unknown): value is { $secret: string } {
|
|
240
|
+
return (
|
|
241
|
+
value !== null &&
|
|
242
|
+
typeof value === 'object' &&
|
|
243
|
+
!Array.isArray(value) &&
|
|
244
|
+
typeof (value as { $secret?: unknown }).$secret === 'string'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** True when a config value should be edited as JSON rather than a plain string. */
|
|
249
|
+
function isJsonConfigValue(value: unknown, field: FieldDefinition): boolean {
|
|
250
|
+
if (typeof value === 'object' && value !== null) return true;
|
|
251
|
+
return value === undefined && (field.type === 'object' || field.type === 'array');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Mono JSON editor for object/array config values — replaces the string
|
|
256
|
+
* Input, which otherwise coerces objects to "[object Object]". Edits are
|
|
257
|
+
* kept in local draft state and only committed to the store on blur, once
|
|
258
|
+
* they parse as valid JSON; invalid JSON is left in the draft with an
|
|
259
|
+
* inline error rather than clobbering the stored value.
|
|
260
|
+
*/
|
|
261
|
+
function JsonConfigField({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
|
|
262
|
+
const updateNodeConfig = useBuilderStore((s) => s.updateNodeConfig);
|
|
263
|
+
const value = node.config[field.name];
|
|
264
|
+
const [draft, setDraft] = useState(() => JSON.stringify(value ?? {}, null, 2));
|
|
265
|
+
const [error, setError] = useState<string | null>(null);
|
|
266
|
+
|
|
267
|
+
// Reset the draft whenever the node, field, or stored value changes from
|
|
268
|
+
// outside this field (node switch, pin, mapping, undo, etc).
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
setDraft(JSON.stringify(value ?? {}, null, 2));
|
|
271
|
+
setError(null);
|
|
272
|
+
}, [node.id, field.name, value]);
|
|
273
|
+
|
|
274
|
+
const rows = Math.min(10, Math.max(3, draft.split('\n').length));
|
|
275
|
+
|
|
276
|
+
const handleBlur = () => {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(draft);
|
|
279
|
+
setError(null);
|
|
280
|
+
updateNodeConfig(node.id, field.name, parsed);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
setError(err instanceof Error ? err.message : 'Invalid JSON');
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className="mb-2">
|
|
288
|
+
<div className="mb-1 flex items-baseline justify-between">
|
|
289
|
+
<FieldName field={field} />
|
|
290
|
+
<FieldTag field={field} />
|
|
291
|
+
</div>
|
|
292
|
+
<textarea
|
|
293
|
+
value={draft}
|
|
294
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
295
|
+
onBlur={handleBlur}
|
|
296
|
+
spellCheck={false}
|
|
297
|
+
rows={rows}
|
|
298
|
+
placeholder="{}"
|
|
299
|
+
className="w-full resize-y rounded-md border border-border bg-background p-3 font-mono text-[12px] leading-relaxed outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
300
|
+
/>
|
|
301
|
+
{error && <div className="mt-1 text-[11px] text-destructive">{error}</div>}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** A Subflow node's workflowId, chosen from the workflows in the workspace. */
|
|
307
|
+
function WorkflowSelectField({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
|
|
308
|
+
const workflows = useBuilderStore((s) => s.workflows);
|
|
309
|
+
const flowId = useBuilderStore((s) => s.flow.id);
|
|
310
|
+
const updateNodeConfig = useBuilderStore((s) => s.updateNodeConfig);
|
|
311
|
+
const value = node.config[field.name];
|
|
312
|
+
// A flow can't call itself (validation error), so leave self out of the list.
|
|
313
|
+
const options: ComboboxOption[] = workflows
|
|
314
|
+
.filter((w) => w.id !== flowId)
|
|
315
|
+
.map((w) => ({ value: w.id, label: w.folder ? `${w.folder} / ${w.name}` : w.name }));
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div className="mb-2 flex items-center justify-between gap-2.5">
|
|
319
|
+
<FieldName field={field} />
|
|
320
|
+
<Combobox
|
|
321
|
+
className="max-w-44"
|
|
322
|
+
options={options}
|
|
323
|
+
value={String(value ?? '')}
|
|
324
|
+
onChange={(v) => updateNodeConfig(node.id, field.name, v)}
|
|
325
|
+
placeholder="select a workflow"
|
|
326
|
+
searchPlaceholder="Search workflows…"
|
|
327
|
+
clearLabel="none"
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function ConfigField({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
|
|
334
|
+
const updateNodeConfig = useBuilderStore((s) => s.updateNodeConfig);
|
|
335
|
+
const value = node.config[field.name];
|
|
336
|
+
|
|
337
|
+
if (node.type === 'Subflow' && field.name === 'workflowId') {
|
|
338
|
+
return <WorkflowSelectField node={node} field={field} />;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isSecretRef(value)) {
|
|
342
|
+
return (
|
|
343
|
+
<div className="mb-2 flex items-center justify-between gap-2.5">
|
|
344
|
+
<FieldName field={field} />
|
|
345
|
+
<Badge variant="mono" title="Resolved from runner secrets at execution time">
|
|
346
|
+
secret · {value.$secret}
|
|
347
|
+
</Badge>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isJsonConfigValue(value, field)) {
|
|
353
|
+
return <JsonConfigField key={`${node.id}:${field.name}`} node={node} field={field} />;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (field.type === 'boolean') {
|
|
357
|
+
return (
|
|
358
|
+
<label className="mb-2 flex items-center justify-between gap-2.5">
|
|
359
|
+
<FieldName field={field} />
|
|
360
|
+
<input
|
|
361
|
+
type="checkbox"
|
|
362
|
+
className="accent-(--highlight)"
|
|
363
|
+
checked={Boolean(value)}
|
|
364
|
+
onChange={(e) => updateNodeConfig(node.id, field.name, e.target.checked)}
|
|
365
|
+
/>
|
|
366
|
+
</label>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (field.type === 'enum') {
|
|
371
|
+
return (
|
|
372
|
+
<div className="mb-2 flex items-center justify-between gap-2.5">
|
|
373
|
+
<FieldName field={field} />
|
|
374
|
+
<Combobox
|
|
375
|
+
className="max-w-40"
|
|
376
|
+
options={(field.enumValues ?? []).map((v) => ({ value: v, label: v }))}
|
|
377
|
+
value={String(value ?? '')}
|
|
378
|
+
onChange={(v) => updateNodeConfig(node.id, field.name, v)}
|
|
379
|
+
clearLabel="none"
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<label className="mb-2 flex items-center justify-between gap-2.5">
|
|
387
|
+
<FieldName field={field} />
|
|
388
|
+
<Input
|
|
389
|
+
className="max-w-40"
|
|
390
|
+
type={field.type === 'number' ? 'number' : 'text'}
|
|
391
|
+
value={String(value ?? '')}
|
|
392
|
+
spellCheck={false}
|
|
393
|
+
onChange={(e) =>
|
|
394
|
+
updateNodeConfig(
|
|
395
|
+
node.id,
|
|
396
|
+
field.name,
|
|
397
|
+
field.type === 'number' ? Number(e.target.value) : e.target.value,
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
/>
|
|
401
|
+
</label>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Read-only input wiring: where each declared input comes from, in plain
|
|
408
|
+
* language. Mappings are agent-authored — the user reads them, never edits
|
|
409
|
+
* them here — so this is a static statement, not a picker: `field ← Source ·
|
|
410
|
+
* out`, or a config default, or an unmapped note.
|
|
411
|
+
*/
|
|
412
|
+
function InputWiringRow({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
|
|
413
|
+
const flow = useBuilderStore((s) => s.flow);
|
|
414
|
+
const mapping = node.inputMap?.[field.name];
|
|
415
|
+
const configValue = node.config[field.name];
|
|
416
|
+
|
|
417
|
+
let source: React.ReactNode;
|
|
418
|
+
if (mapping) {
|
|
419
|
+
const src = flow.nodes.find((n) => n.id === mapping.sourceNodeId);
|
|
420
|
+
const srcLabel = src?.label ?? mapping.sourceNodeId;
|
|
421
|
+
source = (
|
|
422
|
+
<span className="min-w-0 truncate text-[12px] text-foreground/90">
|
|
423
|
+
<span className="text-highlight">{srcLabel}</span>
|
|
424
|
+
{mapping.sourceField !== '$' && (
|
|
425
|
+
<span className="font-mono text-[11px] text-muted-foreground"> · {mapping.sourceField}</span>
|
|
426
|
+
)}
|
|
427
|
+
</span>
|
|
428
|
+
);
|
|
429
|
+
} else if (configValue !== undefined) {
|
|
430
|
+
const text = typeof configValue === 'string' ? configValue : JSON.stringify(configValue);
|
|
431
|
+
source = (
|
|
432
|
+
<code
|
|
433
|
+
className="min-w-0 truncate rounded-sm bg-secondary/60 px-1.5 font-mono text-[11px] text-muted-foreground"
|
|
434
|
+
title={text}
|
|
435
|
+
>
|
|
436
|
+
{text}
|
|
437
|
+
</code>
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
source = <span className="text-[11.5px] text-muted-foreground/50 italic">not wired</span>;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div className="flex items-baseline gap-2 py-1">
|
|
445
|
+
<span className="shrink-0 font-mono text-[11.5px] text-foreground/90">
|
|
446
|
+
{field.name}
|
|
447
|
+
{field.required && <span className="ml-0.5 text-destructive">*</span>}
|
|
448
|
+
</span>
|
|
449
|
+
<span className="shrink-0 text-muted-foreground/50">←</span>
|
|
450
|
+
{source}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The per-node action bar — one consistent row shared by both registers so the
|
|
457
|
+
* controls look and sit the same everywhere: run this node in isolation, view
|
|
458
|
+
* its code, and pin/unpin its last output. `Pin` only appears once there's an
|
|
459
|
+
* output to pin; when pinned it becomes an in-place toggle in the accent colour.
|
|
460
|
+
*/
|
|
461
|
+
function NodeActions({
|
|
462
|
+
onRun,
|
|
463
|
+
onCode,
|
|
464
|
+
hasOutput,
|
|
465
|
+
pinned,
|
|
466
|
+
onPin,
|
|
467
|
+
onUnpin,
|
|
468
|
+
}: {
|
|
469
|
+
onRun: () => void;
|
|
470
|
+
onCode: () => void;
|
|
471
|
+
hasOutput: boolean;
|
|
472
|
+
pinned: boolean;
|
|
473
|
+
onPin: () => void;
|
|
474
|
+
onUnpin: () => void;
|
|
475
|
+
}) {
|
|
476
|
+
const btn = 'gap-1 text-muted-foreground hover:text-foreground';
|
|
477
|
+
return (
|
|
478
|
+
<div className="flex items-center gap-0.5">
|
|
479
|
+
<Button variant="ghost" size="xs" className={btn} onClick={onRun} title="Run this node in isolation">
|
|
480
|
+
<PlayIcon /> Run
|
|
481
|
+
</Button>
|
|
482
|
+
<Button variant="ghost" size="xs" className={btn} onClick={onCode} title="View this node's implementation">
|
|
483
|
+
<CodeIcon /> Code
|
|
484
|
+
</Button>
|
|
485
|
+
{hasOutput &&
|
|
486
|
+
(pinned ? (
|
|
487
|
+
<Button
|
|
488
|
+
variant="ghost"
|
|
489
|
+
size="xs"
|
|
490
|
+
className="gap-1 text-highlight hover:text-highlight"
|
|
491
|
+
onClick={onUnpin}
|
|
492
|
+
title="Pinned — runs reuse this output without executing. Click to unpin."
|
|
493
|
+
>
|
|
494
|
+
<PinIcon className="fill-current" /> Pinned
|
|
495
|
+
</Button>
|
|
496
|
+
) : (
|
|
497
|
+
<Button variant="ghost" size="xs" className={btn} onClick={onPin} title="Pin last output — runs reuse it without executing">
|
|
498
|
+
<PinIcon /> Pin
|
|
499
|
+
</Button>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
|
|
506
|
+
type AuthKind = 'inherit' | 'none' | 'bearer' | 'apiKey';
|
|
507
|
+
|
|
508
|
+
function authKindOf(auth: HttpTrigger['auth']): AuthKind {
|
|
509
|
+
if (auth === undefined || auth === 'inherit') return 'inherit';
|
|
510
|
+
if (auth === 'none') return 'none';
|
|
511
|
+
return auth.scheme;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const baseInputClass =
|
|
515
|
+
'w-full rounded-md border border-border bg-background px-2 py-1.5 text-[12px] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]';
|
|
516
|
+
|
|
517
|
+
// Slug of the flow's name (falling back to the last path segment of its id)
|
|
518
|
+
// used to seed a sensible non-root default path when HTTP is first enabled.
|
|
519
|
+
function defaultHttpPath(flowId: string, flowName: string): string {
|
|
520
|
+
const source = flowName?.trim() || flowId.split('/').pop() || 'operation';
|
|
521
|
+
const slug = source
|
|
522
|
+
.toLowerCase()
|
|
523
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
524
|
+
.replace(/^-+|-+$/g, '');
|
|
525
|
+
return `/${slug || 'operation'}`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Flow-level HTTP endpoint editor: method/path/inputSchema/auth for the
|
|
530
|
+
* active flow's `http` trigger. This is deliberately independent of node
|
|
531
|
+
* selection — any flow can become an operation by adding `http`, so it
|
|
532
|
+
* renders regardless of which (if any) node is selected. Every write goes
|
|
533
|
+
* through `setFlowHttp`, which flows through the same touched()+autosave
|
|
534
|
+
* path (see the store's flow-change subscription) as every other flow edit,
|
|
535
|
+
* so this reuses the existing PUT /workflows/:id persistence — no new path.
|
|
536
|
+
*/
|
|
537
|
+
function HttpSection() {
|
|
538
|
+
const flowId = useBuilderStore((s) => s.flow.id);
|
|
539
|
+
const flowName = useBuilderStore((s) => s.flow.name);
|
|
540
|
+
const http = useBuilderStore((s) => s.flow.http);
|
|
541
|
+
const setFlowHttp = useBuilderStore((s) => s.setFlowHttp);
|
|
542
|
+
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
543
|
+
const [pathHint, setPathHint] = useState<string | null>(null);
|
|
544
|
+
// Holds the raw text of an in-progress, not-yet-valid path edit (e.g. the
|
|
545
|
+
// user cleared the field or typed '/') so the input reflects what they
|
|
546
|
+
// typed instead of snapping back to the last-persisted path. Cleared
|
|
547
|
+
// (falls back to `http.path`) once the text becomes a valid sub-path.
|
|
548
|
+
const [pathDraft, setPathDraft] = useState<string | null>(null);
|
|
549
|
+
|
|
550
|
+
const [schemaDraft, setSchemaDraft] = useState(() => JSON.stringify(http?.inputSchema ?? {}, null, 2));
|
|
551
|
+
const [schemaError, setSchemaError] = useState<string | null>(null);
|
|
552
|
+
|
|
553
|
+
// Reset the schema draft only when the active flow changes — not on every
|
|
554
|
+
// http edit, which would otherwise reformat the textarea under the user's
|
|
555
|
+
// fingers as they type (setFlowHttp round-trips through JSON.stringify).
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
setSchemaDraft(JSON.stringify(http?.inputSchema ?? {}, null, 2));
|
|
558
|
+
setSchemaError(null);
|
|
559
|
+
setPathHint(null);
|
|
560
|
+
setPathDraft(null);
|
|
561
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
562
|
+
}, [flowId]);
|
|
563
|
+
|
|
564
|
+
const enabled = !!http;
|
|
565
|
+
const kind = authKindOf(http?.auth);
|
|
566
|
+
const authObj: AuthPolicy | undefined = http && typeof http.auth === 'object' ? http.auth : undefined;
|
|
567
|
+
|
|
568
|
+
const toggleEnabled = (checked: boolean) => {
|
|
569
|
+
setPathHint(null);
|
|
570
|
+
setPathDraft(null);
|
|
571
|
+
setFlowHttp(checked ? (http ?? { method: 'GET', path: defaultHttpPath(flowId, flowName) }) : undefined);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const handlePathChange = (raw: string) => {
|
|
575
|
+
if (!http) return;
|
|
576
|
+
if (!isMountablePath(raw)) {
|
|
577
|
+
setPathDraft(raw);
|
|
578
|
+
setPathHint('Path must be a sub-path like /things (not /)');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
setPathDraft(null);
|
|
582
|
+
setPathHint(null);
|
|
583
|
+
setFlowHttp({ ...http, path: raw });
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const setAuthKind = (next: AuthKind) => {
|
|
587
|
+
if (!http) return;
|
|
588
|
+
if (next === 'inherit') {
|
|
589
|
+
const { auth: _drop, ...rest } = http;
|
|
590
|
+
setFlowHttp(rest);
|
|
591
|
+
} else if (next === 'none') {
|
|
592
|
+
setFlowHttp({ ...http, auth: 'none' });
|
|
593
|
+
} else {
|
|
594
|
+
setFlowHttp({
|
|
595
|
+
...http,
|
|
596
|
+
auth: { scheme: next, secretRef: authObj?.secretRef ?? '', ...(authObj?.header ? { header: authObj.header } : {}) },
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const handleSchemaChange = (text: string) => {
|
|
602
|
+
setSchemaDraft(text);
|
|
603
|
+
if (!http) return;
|
|
604
|
+
try {
|
|
605
|
+
const parsed = JSON.parse(text);
|
|
606
|
+
setSchemaError(null);
|
|
607
|
+
setFlowHttp({ ...http, inputSchema: parsed });
|
|
608
|
+
} catch (err) {
|
|
609
|
+
setSchemaError(err instanceof Error ? err.message : 'Invalid JSON');
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
615
|
+
<button
|
|
616
|
+
type="button"
|
|
617
|
+
onClick={() => setAdvancedOpen((o) => !o)}
|
|
618
|
+
aria-expanded={advancedOpen}
|
|
619
|
+
className="flex w-full cursor-pointer items-center gap-1.5 text-left text-[10px] font-medium uppercase tracking-widest text-muted-foreground transition-colors hover:text-foreground"
|
|
620
|
+
>
|
|
621
|
+
<ChevronRightIcon
|
|
622
|
+
className={cn('size-3 shrink-0 transition-transform', advancedOpen && 'rotate-90')}
|
|
623
|
+
/>
|
|
624
|
+
Advanced · HTTP & auth
|
|
625
|
+
</button>
|
|
626
|
+
|
|
627
|
+
{advancedOpen && (
|
|
628
|
+
<div className="mt-3">
|
|
629
|
+
<div className="mb-2.5 flex items-center justify-between">
|
|
630
|
+
<SectionTitle>HTTP</SectionTitle>
|
|
631
|
+
<label className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
632
|
+
HTTP endpoint
|
|
633
|
+
<input
|
|
634
|
+
type="checkbox"
|
|
635
|
+
className="accent-(--highlight)"
|
|
636
|
+
checked={enabled}
|
|
637
|
+
onChange={(e) => toggleEnabled(e.target.checked)}
|
|
638
|
+
aria-label="HTTP endpoint"
|
|
639
|
+
/>
|
|
640
|
+
</label>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
{enabled && http && (
|
|
644
|
+
<>
|
|
645
|
+
<div className="mb-2 flex items-center gap-2">
|
|
646
|
+
<select
|
|
647
|
+
className={cn(baseInputClass, 'w-24 shrink-0')}
|
|
648
|
+
value={http.method}
|
|
649
|
+
aria-label="HTTP method"
|
|
650
|
+
onChange={(e) => setFlowHttp({ ...http, method: e.target.value })}
|
|
651
|
+
>
|
|
652
|
+
{HTTP_METHODS.map((m) => (
|
|
653
|
+
<option key={m} value={m}>
|
|
654
|
+
{m}
|
|
655
|
+
</option>
|
|
656
|
+
))}
|
|
657
|
+
</select>
|
|
658
|
+
<input
|
|
659
|
+
className={cn(baseInputClass, 'font-mono')}
|
|
660
|
+
value={pathDraft ?? http.path}
|
|
661
|
+
placeholder="/path/:id"
|
|
662
|
+
spellCheck={false}
|
|
663
|
+
aria-label="HTTP path"
|
|
664
|
+
onChange={(e) => handlePathChange(e.target.value)}
|
|
665
|
+
/>
|
|
666
|
+
</div>
|
|
667
|
+
{pathHint && <div className="mb-2 -mt-1 text-[11px] text-destructive">{pathHint}</div>}
|
|
668
|
+
|
|
669
|
+
<div className="mb-2">
|
|
670
|
+
<div className="mb-1 text-[10.5px] text-muted-foreground">Input schema</div>
|
|
671
|
+
<textarea
|
|
672
|
+
value={schemaDraft}
|
|
673
|
+
onChange={(e) => handleSchemaChange(e.target.value)}
|
|
674
|
+
spellCheck={false}
|
|
675
|
+
rows={Math.min(10, Math.max(3, schemaDraft.split('\n').length))}
|
|
676
|
+
placeholder="{}"
|
|
677
|
+
className="w-full resize-y rounded-md border border-border bg-background p-3 font-mono text-[12px] leading-relaxed outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
678
|
+
/>
|
|
679
|
+
{schemaError && <div className="mt-1 text-[11px] text-destructive">invalid JSON: {schemaError}</div>}
|
|
680
|
+
</div>
|
|
681
|
+
|
|
682
|
+
<div className="mb-2">
|
|
683
|
+
<div className="mb-1 text-[10.5px] text-muted-foreground">Auth</div>
|
|
684
|
+
<select
|
|
685
|
+
className={baseInputClass}
|
|
686
|
+
value={kind}
|
|
687
|
+
aria-label="Auth policy"
|
|
688
|
+
onChange={(e) => setAuthKind(e.target.value as AuthKind)}
|
|
689
|
+
>
|
|
690
|
+
<option value="inherit">Inherit</option>
|
|
691
|
+
<option value="none">None (public)</option>
|
|
692
|
+
<option value="bearer">Bearer token</option>
|
|
693
|
+
<option value="apiKey">API key</option>
|
|
694
|
+
</select>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
{(kind === 'bearer' || kind === 'apiKey') && (
|
|
698
|
+
<div className="mb-2 space-y-2">
|
|
699
|
+
<label className="block">
|
|
700
|
+
<div className="mb-1 text-[10.5px] text-muted-foreground">Secret ref</div>
|
|
701
|
+
<input
|
|
702
|
+
className={cn(baseInputClass, 'font-mono')}
|
|
703
|
+
value={authObj?.secretRef ?? ''}
|
|
704
|
+
spellCheck={false}
|
|
705
|
+
placeholder="e.g. API_TOKEN"
|
|
706
|
+
onChange={(e) =>
|
|
707
|
+
setFlowHttp({
|
|
708
|
+
...http,
|
|
709
|
+
auth: { scheme: kind, secretRef: e.target.value, ...(authObj?.header ? { header: authObj.header } : {}) },
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
/>
|
|
713
|
+
</label>
|
|
714
|
+
{kind === 'apiKey' && (
|
|
715
|
+
<label className="block">
|
|
716
|
+
<div className="mb-1 text-[10.5px] text-muted-foreground">Header (default x-api-key)</div>
|
|
717
|
+
<input
|
|
718
|
+
className={cn(baseInputClass, 'font-mono')}
|
|
719
|
+
value={authObj?.header ?? ''}
|
|
720
|
+
spellCheck={false}
|
|
721
|
+
placeholder="x-api-key"
|
|
722
|
+
onChange={(e) =>
|
|
723
|
+
setFlowHttp({
|
|
724
|
+
...http,
|
|
725
|
+
auth: { scheme: 'apiKey', secretRef: authObj?.secretRef ?? '', header: e.target.value },
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
/>
|
|
729
|
+
</label>
|
|
730
|
+
)}
|
|
731
|
+
</div>
|
|
732
|
+
)}
|
|
733
|
+
</>
|
|
734
|
+
)}
|
|
735
|
+
</div>
|
|
736
|
+
)}
|
|
737
|
+
</section>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function Inspector() {
|
|
742
|
+
const flow = useBuilderStore((s) => s.flow);
|
|
743
|
+
const registry = useBuilderStore((s) => s.registry);
|
|
744
|
+
const run = useBuilderStore((s) => s.run);
|
|
745
|
+
const logs = useBuilderStore((s) => s.logs);
|
|
746
|
+
const register = useBuilderStore((s) => s.viewRegister);
|
|
747
|
+
const selectedNodeId = useBuilderStore((s) => s.selectedNodeId);
|
|
748
|
+
const renameNode = useBuilderStore((s) => s.renameNode);
|
|
749
|
+
const pinNodeOutput = useBuilderStore((s) => s.pinNodeOutput);
|
|
750
|
+
const unpinNode = useBuilderStore((s) => s.unpinNode);
|
|
751
|
+
const setNodeRetry = useBuilderStore((s) => s.setNodeRetry);
|
|
752
|
+
const [codeOpen, setCodeOpen] = useState(false);
|
|
753
|
+
const [runOpen, setRunOpen] = useState(false);
|
|
754
|
+
const [schemaOpen, setSchemaOpen] = useState(false);
|
|
755
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
756
|
+
|
|
757
|
+
// Mechanism (config/mappings/pin/run/code) folds shut whenever the
|
|
758
|
+
// selection changes, so switching nodes never leaks an open fold from one
|
|
759
|
+
// node onto another.
|
|
760
|
+
useEffect(() => {
|
|
761
|
+
setDetailsOpen(false);
|
|
762
|
+
}, [selectedNodeId]);
|
|
763
|
+
|
|
764
|
+
const node = flow.nodes.find((n) => n.id === selectedNodeId);
|
|
765
|
+
|
|
766
|
+
if (!node) {
|
|
767
|
+
return (
|
|
768
|
+
<div className="p-4 pt-6 text-[13px] text-muted-foreground">
|
|
769
|
+
<div className="mb-1.5 font-medium text-foreground/80">Nothing selected</div>
|
|
770
|
+
<p className="leading-relaxed">
|
|
771
|
+
Select a node to inspect its configuration, inputs, outputs, and the data from the
|
|
772
|
+
last run.
|
|
773
|
+
</p>
|
|
774
|
+
<HttpSection />
|
|
775
|
+
</div>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const definition = registry.has(node.type) ? registry.get(node.type).definition : undefined;
|
|
780
|
+
const state = run?.nodeStates[node.id];
|
|
781
|
+
const outputValue = state?.output as Record<string, unknown> | undefined;
|
|
782
|
+
const executions = state?.executions;
|
|
783
|
+
const source = registry.has(node.type) ? (registry.getSource(node.type) ?? '') : '';
|
|
784
|
+
|
|
785
|
+
const codeDialog = (
|
|
786
|
+
<Dialog open={codeOpen} onOpenChange={setCodeOpen}>
|
|
787
|
+
<DialogContent className="max-w-4xl">
|
|
788
|
+
<DialogTitle className="flex items-center gap-2">
|
|
789
|
+
{node.label}
|
|
790
|
+
<Badge variant="mono">{node.type}</Badge>
|
|
791
|
+
</DialogTitle>
|
|
792
|
+
<DialogDescription>{definition?.description ?? 'Node implementation'}</DialogDescription>
|
|
793
|
+
<div className="min-h-0 flex-1 overflow-auto rounded-md border border-border bg-background">
|
|
794
|
+
<SyntaxHighlighter
|
|
795
|
+
language="javascript"
|
|
796
|
+
style={vscDarkPlus}
|
|
797
|
+
showLineNumbers
|
|
798
|
+
customStyle={{
|
|
799
|
+
margin: 0,
|
|
800
|
+
background: 'transparent',
|
|
801
|
+
fontSize: '12.5px',
|
|
802
|
+
lineHeight: 1.6,
|
|
803
|
+
padding: '12px 2px',
|
|
804
|
+
}}
|
|
805
|
+
codeTagProps={{ style: { background: 'transparent', textShadow: 'none' } }}
|
|
806
|
+
lineNumberStyle={{
|
|
807
|
+
color: 'var(--muted-foreground)',
|
|
808
|
+
opacity: 0.35,
|
|
809
|
+
background: 'transparent',
|
|
810
|
+
minWidth: '2.75em',
|
|
811
|
+
paddingRight: '1em',
|
|
812
|
+
}}
|
|
813
|
+
>
|
|
814
|
+
{source}
|
|
815
|
+
</SyntaxHighlighter>
|
|
816
|
+
</div>
|
|
817
|
+
</DialogContent>
|
|
818
|
+
</Dialog>
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
if (register === 'simple') {
|
|
822
|
+
// Failed nodes lead with their error, matching the runbook projection's
|
|
823
|
+
// outcome rule — a stale info line in the outcome slot would misreport.
|
|
824
|
+
const outcome =
|
|
825
|
+
state?.status === 'failed'
|
|
826
|
+
? state.error || lastInfoOutcome(logs, node.id)
|
|
827
|
+
: lastInfoOutcome(logs, node.id);
|
|
828
|
+
const pinned = !!(node.metadata && 'pinnedOutput' in node.metadata);
|
|
829
|
+
const plainDescription = simpleNodeDescription(definition);
|
|
830
|
+
|
|
831
|
+
return (
|
|
832
|
+
<div className="p-4">
|
|
833
|
+
<div className="mb-2">
|
|
834
|
+
<input
|
|
835
|
+
className="-ml-1.5 w-full rounded-sm border border-transparent bg-transparent px-1.5 py-1 text-[15px] font-semibold tracking-tight outline-none transition-colors hover:border-border focus:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
836
|
+
value={node.label}
|
|
837
|
+
onChange={(e) => renameNode(node.id, e.target.value)}
|
|
838
|
+
aria-label="Node label"
|
|
839
|
+
spellCheck={false}
|
|
840
|
+
/>
|
|
841
|
+
<div className="my-1 flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
842
|
+
<span className="font-mono">{node.type}</span>
|
|
843
|
+
{state && (
|
|
844
|
+
<>
|
|
845
|
+
<span className={cn('size-1.5 rounded-full', statusDotClass(state.status))} />
|
|
846
|
+
<span className={statusText(state.status)}>{state.status}</span>
|
|
847
|
+
</>
|
|
848
|
+
)}
|
|
849
|
+
{definition?.effects === 'mutation' && (
|
|
850
|
+
<span className="text-[11px] text-warn" title="Mutation: has side effects">
|
|
851
|
+
⚡
|
|
852
|
+
</span>
|
|
853
|
+
)}
|
|
854
|
+
</div>
|
|
855
|
+
<div className="-ml-1.5 mt-1.5">
|
|
856
|
+
<NodeActions
|
|
857
|
+
onRun={() => setRunOpen(true)}
|
|
858
|
+
onCode={() => setCodeOpen(true)}
|
|
859
|
+
hasOutput={state?.output !== undefined}
|
|
860
|
+
pinned={pinned}
|
|
861
|
+
onPin={() => state?.output !== undefined && pinNodeOutput(node.id, state.output)}
|
|
862
|
+
onUnpin={() => unpinNode(node.id)}
|
|
863
|
+
/>
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
{plainDescription && (
|
|
868
|
+
<p className="mb-3 text-[12.5px] leading-relaxed text-muted-foreground">
|
|
869
|
+
{plainDescription}
|
|
870
|
+
</p>
|
|
871
|
+
)}
|
|
872
|
+
|
|
873
|
+
{outcome && (
|
|
874
|
+
<p className="mb-3 text-[12px] leading-relaxed text-muted-foreground/80 italic">{outcome}</p>
|
|
875
|
+
)}
|
|
876
|
+
|
|
877
|
+
{(() => {
|
|
878
|
+
const inputVal = state?.input as Record<string, unknown> | undefined;
|
|
879
|
+
const hasInputVal = !!inputVal && Object.keys(inputVal).length > 0;
|
|
880
|
+
const inputShape = shapeFields(node, definition, 'input');
|
|
881
|
+
// Nothing to say — no run value and no declared shape — so no section.
|
|
882
|
+
if (!hasInputVal && inputShape.length === 0) return null;
|
|
883
|
+
return (
|
|
884
|
+
<section className="mt-3 border-t border-border pt-3">
|
|
885
|
+
<SectionTitle>Inputs</SectionTitle>
|
|
886
|
+
{hasInputVal ? <KeyValueGrid value={inputVal} emptyLabel="" /> : <ShapeList fields={inputShape} />}
|
|
887
|
+
</section>
|
|
888
|
+
);
|
|
889
|
+
})()}
|
|
890
|
+
|
|
891
|
+
{(() => {
|
|
892
|
+
const hasExecOutputs = !!executions && executions.length > 1;
|
|
893
|
+
const hasOutputVal = !!outputValue && Object.keys(outputValue).length > 0;
|
|
894
|
+
const outputShape = shapeFields(node, definition, 'output');
|
|
895
|
+
if (!hasExecOutputs && !hasOutputVal && !state?.error && outputShape.length === 0) return null;
|
|
896
|
+
return (
|
|
897
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
898
|
+
<SectionTitle>Outputs</SectionTitle>
|
|
899
|
+
{hasExecOutputs ? (
|
|
900
|
+
<ExecutionPager executions={executions!}>
|
|
901
|
+
{(record) => (
|
|
902
|
+
<KeyValueGrid
|
|
903
|
+
value={record.output as Record<string, unknown> | undefined}
|
|
904
|
+
error={record.error}
|
|
905
|
+
emptyLabel=""
|
|
906
|
+
/>
|
|
907
|
+
)}
|
|
908
|
+
</ExecutionPager>
|
|
909
|
+
) : hasOutputVal || state?.error ? (
|
|
910
|
+
<KeyValueGrid value={outputValue} error={state?.error} emptyLabel="" />
|
|
911
|
+
) : (
|
|
912
|
+
<ShapeList fields={outputShape} />
|
|
913
|
+
)}
|
|
914
|
+
</section>
|
|
915
|
+
);
|
|
916
|
+
})()}
|
|
917
|
+
|
|
918
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
919
|
+
<button
|
|
920
|
+
type="button"
|
|
921
|
+
onClick={() => setDetailsOpen((o) => !o)}
|
|
922
|
+
aria-expanded={detailsOpen}
|
|
923
|
+
className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground transition-colors hover:text-foreground"
|
|
924
|
+
>
|
|
925
|
+
<ChevronRightIcon
|
|
926
|
+
className={cn('size-3 shrink-0 transition-transform', detailsOpen && 'rotate-90')}
|
|
927
|
+
/>
|
|
928
|
+
Details
|
|
929
|
+
</button>
|
|
930
|
+
|
|
931
|
+
{detailsOpen && (
|
|
932
|
+
<div className="mt-3">
|
|
933
|
+
{(definition?.configSchema?.fields.length ?? 0) > 0 && (
|
|
934
|
+
<section className="mb-4">
|
|
935
|
+
<SectionTitle>Config</SectionTitle>
|
|
936
|
+
{node.type === 'Conditional' ? (
|
|
937
|
+
<BranchRulesEditor node={node} />
|
|
938
|
+
) : (
|
|
939
|
+
definition!.configSchema!.fields.map((f) => (
|
|
940
|
+
<ConfigField key={f.name} node={node} field={f} />
|
|
941
|
+
))
|
|
942
|
+
)}
|
|
943
|
+
</section>
|
|
944
|
+
)}
|
|
945
|
+
|
|
946
|
+
{(definition?.inputSchema?.fields.length ?? 0) > 0 && (
|
|
947
|
+
<section className="mb-4">
|
|
948
|
+
<SectionTitle>Input mappings</SectionTitle>
|
|
949
|
+
{definition!.inputSchema!.fields.map((f) => (
|
|
950
|
+
<InputWiringRow key={f.name} node={node} field={f} />
|
|
951
|
+
))}
|
|
952
|
+
</section>
|
|
953
|
+
)}
|
|
954
|
+
</div>
|
|
955
|
+
)}
|
|
956
|
+
</section>
|
|
957
|
+
|
|
958
|
+
<HttpSection />
|
|
959
|
+
|
|
960
|
+
<NodeRunModal node={node} open={runOpen} onOpenChange={setRunOpen} />
|
|
961
|
+
{codeDialog}
|
|
962
|
+
</div>
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return (
|
|
967
|
+
<div className="p-4">
|
|
968
|
+
<div className="mb-2">
|
|
969
|
+
<input
|
|
970
|
+
className="-ml-1.5 w-full rounded-sm border border-transparent bg-transparent px-1.5 py-1 text-[15px] font-semibold tracking-tight outline-none transition-colors hover:border-border focus:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
971
|
+
value={node.label}
|
|
972
|
+
onChange={(e) => renameNode(node.id, e.target.value)}
|
|
973
|
+
aria-label="Node label"
|
|
974
|
+
spellCheck={false}
|
|
975
|
+
/>
|
|
976
|
+
<div className="my-1 flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
977
|
+
<span className="font-mono">{node.type}</span>
|
|
978
|
+
{state && (
|
|
979
|
+
<>
|
|
980
|
+
<span className="text-border">·</span>
|
|
981
|
+
<span className={statusText(state.status)}>{state.status}</span>
|
|
982
|
+
</>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
<div className="-ml-1.5 mt-1.5">
|
|
986
|
+
<NodeActions
|
|
987
|
+
onRun={() => setRunOpen(true)}
|
|
988
|
+
onCode={() => setCodeOpen(true)}
|
|
989
|
+
hasOutput={state?.output !== undefined}
|
|
990
|
+
pinned={!!(node.metadata && 'pinnedOutput' in node.metadata)}
|
|
991
|
+
onPin={() => state?.output !== undefined && pinNodeOutput(node.id, state.output)}
|
|
992
|
+
onUnpin={() => unpinNode(node.id)}
|
|
993
|
+
/>
|
|
994
|
+
</div>
|
|
995
|
+
<NodeRunModal node={node} open={runOpen} onOpenChange={setRunOpen} />
|
|
996
|
+
{codeDialog}
|
|
997
|
+
{definition?.description && (
|
|
998
|
+
<p className="mt-1.5 text-[12.5px] leading-relaxed text-muted-foreground">
|
|
999
|
+
{definition.description}
|
|
1000
|
+
</p>
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
|
|
1004
|
+
{/* The real call this node makes — same kind + endpoint/query/model the
|
|
1005
|
+
runbook badge reveals on hover, surfaced here so it's readable without
|
|
1006
|
+
hunting for the badge. */}
|
|
1007
|
+
{definition?.traceKind && definition.traceKind !== 'compute' && (
|
|
1008
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
1009
|
+
<div className="mb-2 flex items-center gap-2">
|
|
1010
|
+
<span
|
|
1011
|
+
className="rounded px-1.5 py-0.5 font-mono text-[9px] font-semibold tracking-wide"
|
|
1012
|
+
style={{
|
|
1013
|
+
color: TRACE_KIND_COLOR[definition.traceKind],
|
|
1014
|
+
backgroundColor: `${TRACE_KIND_COLOR[definition.traceKind]}1f`,
|
|
1015
|
+
}}
|
|
1016
|
+
>
|
|
1017
|
+
{TRACE_KIND_LABEL[definition.traceKind]}
|
|
1018
|
+
</span>
|
|
1019
|
+
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
|
|
1020
|
+
{TRACE_KIND_DETAIL_LABEL[definition.traceKind]}
|
|
1021
|
+
</span>
|
|
1022
|
+
{definition.effects === 'mutation' && (
|
|
1023
|
+
<span className="text-[11px] text-warn" title="Mutation: has side effects">⚡</span>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
{definition.traceDetail ? (
|
|
1027
|
+
<p className="font-mono text-[11.5px] leading-relaxed break-words text-foreground/85">
|
|
1028
|
+
{definition.traceDetail}
|
|
1029
|
+
</p>
|
|
1030
|
+
) : (
|
|
1031
|
+
<p className="text-[11.5px] text-muted-foreground/60">No trace detail recorded.</p>
|
|
1032
|
+
)}
|
|
1033
|
+
</section>
|
|
1034
|
+
)}
|
|
1035
|
+
|
|
1036
|
+
{(definition?.configSchema?.fields.length ?? 0) > 0 && (
|
|
1037
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
1038
|
+
<SectionTitle>Config</SectionTitle>
|
|
1039
|
+
{node.type === 'Conditional' ? (
|
|
1040
|
+
<BranchRulesEditor node={node} />
|
|
1041
|
+
) : (
|
|
1042
|
+
definition!.configSchema!.fields.map((f) => (
|
|
1043
|
+
<ConfigField key={f.name} node={node} field={f} />
|
|
1044
|
+
))
|
|
1045
|
+
)}
|
|
1046
|
+
</section>
|
|
1047
|
+
)}
|
|
1048
|
+
|
|
1049
|
+
{node.type !== 'Input' && node.type !== 'Response' && node.type !== 'Result' && (
|
|
1050
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
1051
|
+
<SectionTitle>Retry</SectionTitle>
|
|
1052
|
+
<div className="mt-1.5 flex items-center gap-2 text-[11.5px] text-muted-foreground">
|
|
1053
|
+
<input
|
|
1054
|
+
type="number"
|
|
1055
|
+
min={1}
|
|
1056
|
+
value={node.retry?.maxTries ?? ''}
|
|
1057
|
+
placeholder="1"
|
|
1058
|
+
onChange={(e) => {
|
|
1059
|
+
const maxTries = Number(e.target.value);
|
|
1060
|
+
setNodeRetry(
|
|
1061
|
+
node.id,
|
|
1062
|
+
Number.isInteger(maxTries) && maxTries > 1
|
|
1063
|
+
? { maxTries, ...(node.retry?.waitMs ? { waitMs: node.retry.waitMs } : {}) }
|
|
1064
|
+
: undefined,
|
|
1065
|
+
);
|
|
1066
|
+
}}
|
|
1067
|
+
className="w-14 rounded-md border border-border bg-background px-2 py-1 font-mono text-[12px] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
1068
|
+
aria-label="Total attempts"
|
|
1069
|
+
title="Total attempts including the first — 1 or empty disables retry"
|
|
1070
|
+
/>
|
|
1071
|
+
<span>tries</span>
|
|
1072
|
+
<input
|
|
1073
|
+
type="number"
|
|
1074
|
+
min={0}
|
|
1075
|
+
step={100}
|
|
1076
|
+
value={node.retry?.waitMs ?? ''}
|
|
1077
|
+
placeholder="0"
|
|
1078
|
+
disabled={!node.retry}
|
|
1079
|
+
onChange={(e) => {
|
|
1080
|
+
if (!node.retry) return;
|
|
1081
|
+
const waitMs = Number(e.target.value);
|
|
1082
|
+
setNodeRetry(node.id, {
|
|
1083
|
+
maxTries: node.retry.maxTries,
|
|
1084
|
+
...(Number.isFinite(waitMs) && waitMs > 0 ? { waitMs } : {}),
|
|
1085
|
+
});
|
|
1086
|
+
}}
|
|
1087
|
+
className="w-20 rounded-md border border-border bg-background px-2 py-1 font-mono text-[12px] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:opacity-40"
|
|
1088
|
+
aria-label="Wait between attempts (ms)"
|
|
1089
|
+
title="Fixed wait between attempts, in milliseconds"
|
|
1090
|
+
/>
|
|
1091
|
+
<span>ms between</span>
|
|
1092
|
+
</div>
|
|
1093
|
+
</section>
|
|
1094
|
+
)}
|
|
1095
|
+
|
|
1096
|
+
{/* Run I/O leads in technical: when the node has actually executed, the
|
|
1097
|
+
run's real output (then input) as formatted JSON is what a developer
|
|
1098
|
+
came to see — declarations and mappings are agent territory and fold
|
|
1099
|
+
below. */}
|
|
1100
|
+
{state && (state.output !== undefined || state.input !== undefined || state.error) && (
|
|
1101
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
1102
|
+
<SectionTitle>Run I/O</SectionTitle>
|
|
1103
|
+
{executions && executions.length > 1 ? (
|
|
1104
|
+
<ExecutionPager executions={executions}>
|
|
1105
|
+
{(record) => (
|
|
1106
|
+
<RunIoPanes input={record.input} output={record.output} error={record.error} />
|
|
1107
|
+
)}
|
|
1108
|
+
</ExecutionPager>
|
|
1109
|
+
) : (
|
|
1110
|
+
<RunIoPanes input={state.input} output={state.output} error={state.error} />
|
|
1111
|
+
)}
|
|
1112
|
+
</section>
|
|
1113
|
+
)}
|
|
1114
|
+
|
|
1115
|
+
<section className="mt-4 border-t border-border pt-3">
|
|
1116
|
+
<button
|
|
1117
|
+
type="button"
|
|
1118
|
+
onClick={() => setSchemaOpen((v) => !v)}
|
|
1119
|
+
aria-expanded={schemaOpen}
|
|
1120
|
+
className="flex w-full cursor-pointer items-center gap-1.5 text-left"
|
|
1121
|
+
>
|
|
1122
|
+
<ChevronRightIcon
|
|
1123
|
+
className={cn('size-3 text-muted-foreground transition-transform', schemaOpen && 'rotate-90')}
|
|
1124
|
+
/>
|
|
1125
|
+
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
|
|
1126
|
+
Wiring & schema
|
|
1127
|
+
</span>
|
|
1128
|
+
</button>
|
|
1129
|
+
{schemaOpen && (
|
|
1130
|
+
<div className="mt-3">
|
|
1131
|
+
{(definition?.inputSchema?.fields.length ?? 0) > 0 && (
|
|
1132
|
+
<div className="mb-4">
|
|
1133
|
+
<SectionTitle>Input wiring</SectionTitle>
|
|
1134
|
+
{definition!.inputSchema!.fields.map((f) => (
|
|
1135
|
+
<InputWiringRow key={f.name} node={node} field={f} />
|
|
1136
|
+
))}
|
|
1137
|
+
</div>
|
|
1138
|
+
)}
|
|
1139
|
+
<SectionTitle>Outputs</SectionTitle>
|
|
1140
|
+
<OutputFieldsList
|
|
1141
|
+
fields={definition?.outputSchema?.fields ?? []}
|
|
1142
|
+
outputValue={outputValue}
|
|
1143
|
+
error={state?.error}
|
|
1144
|
+
/>
|
|
1145
|
+
</div>
|
|
1146
|
+
)}
|
|
1147
|
+
</section>
|
|
1148
|
+
|
|
1149
|
+
<HttpSection />
|
|
1150
|
+
</div>
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Technical Run I/O: the run's actual payloads as formatted JSON — output
|
|
1155
|
+
* first (what the node produced is the headline), then input, error verbatim. */
|
|
1156
|
+
function RunIoPanes({ input, output, error }: { input: unknown; output: unknown; error?: string }) {
|
|
1157
|
+
return (
|
|
1158
|
+
<div className="space-y-3">
|
|
1159
|
+
{error && (
|
|
1160
|
+
<pre className="overflow-x-auto rounded-md border border-destructive/50 bg-background p-2.5 font-mono text-[11.5px] whitespace-pre-wrap text-destructive-foreground">
|
|
1161
|
+
{error}
|
|
1162
|
+
</pre>
|
|
1163
|
+
)}
|
|
1164
|
+
{output !== undefined && (
|
|
1165
|
+
<div>
|
|
1166
|
+
<div className="mb-1 text-[9.5px] font-medium uppercase tracking-widest text-muted-foreground/70">
|
|
1167
|
+
Output
|
|
1168
|
+
</div>
|
|
1169
|
+
<Json value={output} maxHeight={280} />
|
|
1170
|
+
</div>
|
|
1171
|
+
)}
|
|
1172
|
+
{input !== undefined && (
|
|
1173
|
+
<div>
|
|
1174
|
+
<div className="mb-1 text-[9.5px] font-medium uppercase tracking-widest text-muted-foreground/70">
|
|
1175
|
+
Input
|
|
1176
|
+
</div>
|
|
1177
|
+
<Json value={input} maxHeight={200} />
|
|
1178
|
+
</div>
|
|
1179
|
+
)}
|
|
1180
|
+
</div>
|
|
1181
|
+
);
|
|
1182
|
+
}
|