@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,663 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ChevronDownIcon,
|
|
4
|
+
DownloadIcon,
|
|
5
|
+
FlameIcon,
|
|
6
|
+
PencilIcon,
|
|
7
|
+
EllipsisIcon,
|
|
8
|
+
PlayIcon,
|
|
9
|
+
PlusIcon,
|
|
10
|
+
SaveIcon,
|
|
11
|
+
SparklesIcon,
|
|
12
|
+
SquareIcon,
|
|
13
|
+
StepForwardIcon,
|
|
14
|
+
TerminalSquareIcon,
|
|
15
|
+
TriangleAlertIcon,
|
|
16
|
+
UploadIcon,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { cn } from '@/lib/utils';
|
|
19
|
+
import { diagnoseOperation, type DiagnoseOperationExtras } from '../engine/diagnostics';
|
|
20
|
+
import type { NodeRegistry } from '../engine';
|
|
21
|
+
import { Button } from '@/components/ui/button';
|
|
22
|
+
import {
|
|
23
|
+
Dialog,
|
|
24
|
+
DialogContent,
|
|
25
|
+
DialogTitle,
|
|
26
|
+
} from '@/components/ui/dialog';
|
|
27
|
+
import { Input } from '@/components/ui/input';
|
|
28
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
29
|
+
import { useBuilderStore } from '../store/builderStore';
|
|
30
|
+
import { EnvironmentPicker, useRunnerPolling } from './EnvironmentPicker';
|
|
31
|
+
import { useRunConsole } from './RunLogPanel';
|
|
32
|
+
import { SettingsDialog } from './SettingsDialog';
|
|
33
|
+
import { WelcomeDialog } from './WelcomeDialog';
|
|
34
|
+
import { ScenariosPanel } from './Scenarios';
|
|
35
|
+
import type { ScenarioDefinition } from '../engine';
|
|
36
|
+
|
|
37
|
+
/** Derives diagnoseOperation's `infraNodes` extra from the studio's client-side
|
|
38
|
+
* node registry (synced from the runner via syncNodeMeta, plus the bundled
|
|
39
|
+
* built-ins present from module load). A node whose type isn't yet in the
|
|
40
|
+
* registry is skipped rather than aborting the whole computation — the
|
|
41
|
+
* degradation is per-node, not all-or-nothing. */
|
|
42
|
+
function infraNodesOf(
|
|
43
|
+
flow: { nodes: Array<{ id: string; type: string }> },
|
|
44
|
+
registry: NodeRegistry,
|
|
45
|
+
): DiagnoseOperationExtras['infraNodes'] {
|
|
46
|
+
const infraNodes: NonNullable<DiagnoseOperationExtras['infraNodes']> = [];
|
|
47
|
+
for (const node of flow.nodes) {
|
|
48
|
+
if (!registry.has(node.type)) continue;
|
|
49
|
+
const { traceKind } = registry.get(node.type).definition;
|
|
50
|
+
if (traceKind === 'db' || traceKind === 'http' || traceKind === 'llm') {
|
|
51
|
+
infraNodes.push({ id: node.id, traceKind });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return infraNodes;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Derives diagnoseOperation's `mutationSourcesByNode` extra from the studio's
|
|
58
|
+
* client-side node registry, mirroring infraNodesOf's per-node degradation:
|
|
59
|
+
* a node whose type isn't registered, isn't a mutation, or whose
|
|
60
|
+
* implementation isn't a function is simply absent from the map rather than
|
|
61
|
+
* aborting the whole computation. */
|
|
62
|
+
function mutationSourcesOf(
|
|
63
|
+
flow: { nodes: Array<{ id: string; type: string }> },
|
|
64
|
+
registry: NodeRegistry,
|
|
65
|
+
): DiagnoseOperationExtras['mutationSourcesByNode'] {
|
|
66
|
+
const mutationSourcesByNode: NonNullable<DiagnoseOperationExtras['mutationSourcesByNode']> = {};
|
|
67
|
+
for (const node of flow.nodes) {
|
|
68
|
+
if (!registry.has(node.type)) continue;
|
|
69
|
+
const { definition, implementation } = registry.get(node.type);
|
|
70
|
+
if (definition.effects === 'mutation' && typeof implementation === 'function') {
|
|
71
|
+
mutationSourcesByNode[node.id] = String(implementation);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return mutationSourcesByNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const statusDot: Record<string, string> = {
|
|
78
|
+
succeeded: 'bg-success',
|
|
79
|
+
failed: 'bg-destructive',
|
|
80
|
+
cancelled: 'bg-muted-foreground',
|
|
81
|
+
running: 'bg-highlight animate-pulse',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run split button: the main segment runs the flow to the end; the attached
|
|
86
|
+
* chevron opens the scenario menu (Patrick's "play button with a dropdown"),
|
|
87
|
+
* replacing the old Scenarios dock tab. Turns destructive-red when a run would
|
|
88
|
+
* execute writes against a protected environment.
|
|
89
|
+
*/
|
|
90
|
+
function RunSplitButton({
|
|
91
|
+
onRun,
|
|
92
|
+
runDisabled,
|
|
93
|
+
live,
|
|
94
|
+
stepping,
|
|
95
|
+
}: {
|
|
96
|
+
onRun: () => void;
|
|
97
|
+
runDisabled: boolean;
|
|
98
|
+
live: boolean;
|
|
99
|
+
stepping: boolean;
|
|
100
|
+
}) {
|
|
101
|
+
const flow = useBuilderStore((s) => s.flow);
|
|
102
|
+
const runHistory = useBuilderStore((s) => s.runHistory);
|
|
103
|
+
const runScenario = useBuilderStore((s) => s.runScenario);
|
|
104
|
+
const stepScenario = useBuilderStore((s) => s.stepScenario);
|
|
105
|
+
const runnerMock = useBuilderStore((s) => s.runnerMock);
|
|
106
|
+
const runAgent = useBuilderStore((s) => s.runAgent);
|
|
107
|
+
const agentRunning = useBuilderStore((s) => s.agentRun?.status === 'running');
|
|
108
|
+
const [open, setOpen] = useState(false);
|
|
109
|
+
const [manage, setManage] = useState<'manage' | 'new' | null>(null);
|
|
110
|
+
const [aiOpen, setAiOpen] = useState(false);
|
|
111
|
+
const [aiInstruction, setAiInstruction] = useState('');
|
|
112
|
+
|
|
113
|
+
const scenarios = flow.scenarios ?? [];
|
|
114
|
+
const variant = live ? 'destructive' : 'default';
|
|
115
|
+
const paramBlockers = diagnoseOperation(flow, scenarios).filter(
|
|
116
|
+
(d) => d.code === 'missing-param-default',
|
|
117
|
+
);
|
|
118
|
+
const paramsBlocked = paramBlockers.length > 0;
|
|
119
|
+
// The params guard only blocks STARTING a fresh plain run (which sends no
|
|
120
|
+
// request, so path params would be undefined). While stepping, the run is
|
|
121
|
+
// already under way with real input — "Finish" must stay clickable.
|
|
122
|
+
const mainRunDisabled = runDisabled || (paramsBlocked && !stepping);
|
|
123
|
+
const selectedEnvironment = useBuilderStore((s) => s.selectedEnvironment);
|
|
124
|
+
const safeMode = useBuilderStore((s) => s.safeMode);
|
|
125
|
+
// Blocked wins (it says why the button is disabled); otherwise plain-terms
|
|
126
|
+
// explainer of what a real run means.
|
|
127
|
+
const runTitle = paramsBlocked
|
|
128
|
+
? paramBlockers.map((d) => d.message).join('\n')
|
|
129
|
+
: stepping
|
|
130
|
+
? 'Runs the stepped run to the end.'
|
|
131
|
+
: runnerMock
|
|
132
|
+
? 'Runs this operation against scenario mocks — real logic, canned infrastructure. Nothing real is touched.'
|
|
133
|
+
: `Runs this operation for real against "${selectedEnvironment || 'browser'}" — every node executes, and requests hit real services. Safe mode ${safeMode ? 'is on: writes are skipped' : 'is off: writes happen'}.`;
|
|
134
|
+
|
|
135
|
+
const lastRunFor = (sc: ScenarioDefinition) =>
|
|
136
|
+
runHistory.find((r) => r.workflowId === flow.id && r.scenarioName === sc.name);
|
|
137
|
+
|
|
138
|
+
const play = (id: string) => {
|
|
139
|
+
void runScenario(id);
|
|
140
|
+
setOpen(false);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const step = (id: string) => {
|
|
144
|
+
void stepScenario(id);
|
|
145
|
+
setOpen(false);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const submitAiScenario = () => {
|
|
149
|
+
const text = aiInstruction.trim();
|
|
150
|
+
if (!text) return;
|
|
151
|
+
void runAgent({ action: 'new-scenario', flowId: flow.id, instruction: text });
|
|
152
|
+
setAiInstruction('');
|
|
153
|
+
setAiOpen(false);
|
|
154
|
+
setOpen(false);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const coverWithAi = () => {
|
|
158
|
+
setOpen(false);
|
|
159
|
+
void runAgent({
|
|
160
|
+
action: 'cover-operation',
|
|
161
|
+
flowId: flow.id,
|
|
162
|
+
instruction: 'Cover this operation with a branch-covering scenario suite; every scenario gets an expect.',
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const dropdownTrigger = (
|
|
167
|
+
<Button
|
|
168
|
+
size="sm"
|
|
169
|
+
variant={variant}
|
|
170
|
+
className="gap-1 rounded-l-none border-l border-black/20 px-1.5"
|
|
171
|
+
disabled={runDisabled}
|
|
172
|
+
aria-label="Run a scenario"
|
|
173
|
+
>
|
|
174
|
+
{scenarios.length > 0 && (
|
|
175
|
+
<span className="rounded bg-black/15 px-1 font-mono text-[9px] leading-tight">
|
|
176
|
+
{scenarios.length}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
<ChevronDownIcon className="size-3 opacity-80" />
|
|
180
|
+
</Button>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<>
|
|
185
|
+
<div className="flex items-center">
|
|
186
|
+
<Button
|
|
187
|
+
size="sm"
|
|
188
|
+
variant={variant}
|
|
189
|
+
className={cn('rounded-r-none', runnerMock && !live && 'bg-warn/85 text-black hover:bg-warn')}
|
|
190
|
+
onClick={onRun}
|
|
191
|
+
disabled={mainRunDisabled}
|
|
192
|
+
title={runTitle}
|
|
193
|
+
>
|
|
194
|
+
{stepping ? <SquareIcon /> : <PlayIcon />} {stepping ? 'Finish' : 'Run'}
|
|
195
|
+
</Button>
|
|
196
|
+
<Popover
|
|
197
|
+
open={open}
|
|
198
|
+
onOpenChange={(o) => {
|
|
199
|
+
setOpen(o);
|
|
200
|
+
if (!o) {
|
|
201
|
+
setAiOpen(false);
|
|
202
|
+
setAiInstruction('');
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<PopoverTrigger asChild>{dropdownTrigger}</PopoverTrigger>
|
|
207
|
+
<PopoverContent className="w-[26rem] max-w-[calc(100vw-2rem)] p-1" align="end">
|
|
208
|
+
{scenarios.length === 0 ? (
|
|
209
|
+
<div className="flex flex-col gap-1.5 px-2.5 py-2">
|
|
210
|
+
<span className="text-[12px] text-muted-foreground/80">
|
|
211
|
+
No scenarios yet — named inputs (with mocks and expects) to run this flow
|
|
212
|
+
through. Cover with AI writes a starter suite.
|
|
213
|
+
</span>
|
|
214
|
+
<Button size="sm" variant="secondary" disabled={agentRunning} onClick={coverWithAi}>
|
|
215
|
+
<SparklesIcon className="size-3.5" />
|
|
216
|
+
Cover with AI
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
) : (
|
|
220
|
+
<div className="flex flex-col">
|
|
221
|
+
{scenarios.map((sc) => {
|
|
222
|
+
const last = lastRunFor(sc);
|
|
223
|
+
return (
|
|
224
|
+
<div key={sc.id} className="group flex items-start rounded-sm transition-colors hover:bg-accent">
|
|
225
|
+
<button
|
|
226
|
+
onClick={() => play(sc.id)}
|
|
227
|
+
title={runnerMock ? 'Runs this scenario against its mocks — nothing real is touched.' : undefined}
|
|
228
|
+
className="flex min-w-0 flex-1 cursor-pointer items-start gap-2 px-2 py-1.5 text-left"
|
|
229
|
+
>
|
|
230
|
+
<PlayIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
231
|
+
<span
|
|
232
|
+
className={cn(
|
|
233
|
+
'mt-[7px] size-1.5 shrink-0 rounded-full',
|
|
234
|
+
last ? statusDot[last.status] : 'bg-border',
|
|
235
|
+
)}
|
|
236
|
+
title={last ? `Last run ${last.status}` : 'Not run yet'}
|
|
237
|
+
/>
|
|
238
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
239
|
+
<span className="text-[12.5px] font-medium leading-tight">{sc.name}</span>
|
|
240
|
+
{sc.description && (
|
|
241
|
+
<span className="text-[11.5px] leading-snug text-muted-foreground">
|
|
242
|
+
{sc.description}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
</span>
|
|
246
|
+
</button>
|
|
247
|
+
<button
|
|
248
|
+
onClick={() => step(sc.id)}
|
|
249
|
+
title="Step through this scenario"
|
|
250
|
+
aria-label={`Step through ${sc.name}`}
|
|
251
|
+
className="mt-1 mr-1 shrink-0 cursor-pointer rounded-sm p-1.5 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 hover:bg-secondary hover:text-foreground focus-visible:opacity-100"
|
|
252
|
+
>
|
|
253
|
+
<StepForwardIcon className="size-3.5" />
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
})}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
<div className="my-1 h-px bg-border" />
|
|
261
|
+
{aiOpen ? (
|
|
262
|
+
<div className="flex items-center gap-1.5 px-2 py-1.5">
|
|
263
|
+
<Input
|
|
264
|
+
autoFocus
|
|
265
|
+
value={aiInstruction}
|
|
266
|
+
onChange={(e) => setAiInstruction(e.target.value)}
|
|
267
|
+
onKeyDown={(e) => {
|
|
268
|
+
if (e.key === 'Enter') submitAiScenario();
|
|
269
|
+
}}
|
|
270
|
+
placeholder="e.g. cover the empty-title case"
|
|
271
|
+
className="h-8 text-[12.5px]"
|
|
272
|
+
/>
|
|
273
|
+
<Button size="sm" onClick={submitAiScenario} disabled={!aiInstruction.trim()}>
|
|
274
|
+
Go
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
) : (
|
|
278
|
+
<button
|
|
279
|
+
onClick={() => setAiOpen(true)}
|
|
280
|
+
disabled={agentRunning}
|
|
281
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-[12.5px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
282
|
+
>
|
|
283
|
+
<SparklesIcon className="size-3.5" />
|
|
284
|
+
New scenario with AI…
|
|
285
|
+
</button>
|
|
286
|
+
)}
|
|
287
|
+
<button
|
|
288
|
+
onClick={() => {
|
|
289
|
+
setManage('manage');
|
|
290
|
+
setOpen(false);
|
|
291
|
+
}}
|
|
292
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-[12.5px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
293
|
+
>
|
|
294
|
+
<PencilIcon className="size-3.5" />
|
|
295
|
+
Manage scenarios…
|
|
296
|
+
</button>
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => {
|
|
299
|
+
setManage('new');
|
|
300
|
+
setOpen(false);
|
|
301
|
+
}}
|
|
302
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-[12.5px] text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
303
|
+
>
|
|
304
|
+
<PlusIcon className="size-3.5" />
|
|
305
|
+
New scenario…
|
|
306
|
+
</button>
|
|
307
|
+
</PopoverContent>
|
|
308
|
+
</Popover>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<Dialog open={manage !== null} onOpenChange={(o) => !o && setManage(null)}>
|
|
312
|
+
<DialogContent className="max-w-2xl">
|
|
313
|
+
<DialogTitle>Scenarios</DialogTitle>
|
|
314
|
+
<div className="max-h-[65vh] overflow-auto">
|
|
315
|
+
<ScenariosPanel autoNew={manage === 'new'} />
|
|
316
|
+
</div>
|
|
317
|
+
</DialogContent>
|
|
318
|
+
</Dialog>
|
|
319
|
+
</>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
const severityDot: Record<string, string> = {
|
|
325
|
+
error: 'bg-destructive',
|
|
326
|
+
warning: 'bg-warn',
|
|
327
|
+
info: 'bg-muted-foreground',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Problems chip: amber triangle + count when the current operation has
|
|
332
|
+
* diagnostics (missing param defaults, uncovered params, no expects). The
|
|
333
|
+
* diagnostic messages themselves are the helper copy — each says what is
|
|
334
|
+
* wrong and what to do about it. Renders nothing when the operation is clean.
|
|
335
|
+
*/
|
|
336
|
+
function ProblemsChip() {
|
|
337
|
+
const flow = useBuilderStore((s) => s.flow);
|
|
338
|
+
const registry = useBuilderStore((s) => s.registry);
|
|
339
|
+
const seedParamDefault = useBuilderStore((s) => s.seedParamDefault);
|
|
340
|
+
const runAgent = useBuilderStore((s) => s.runAgent);
|
|
341
|
+
const agentRunning = useBuilderStore((s) => s.agentRun?.status === 'running');
|
|
342
|
+
const [open, setOpen] = useState(false);
|
|
343
|
+
const diagnostics = diagnoseOperation(flow, flow.scenarios, {
|
|
344
|
+
infraNodes: infraNodesOf(flow, registry),
|
|
345
|
+
mutationSourcesByNode: mutationSourcesOf(flow, registry),
|
|
346
|
+
});
|
|
347
|
+
if (diagnostics.length === 0) return null;
|
|
348
|
+
|
|
349
|
+
const fixWithAi = (action: 'new-scenario' | 'cover-operation', instruction: string) => {
|
|
350
|
+
setOpen(false);
|
|
351
|
+
void runAgent({ action, flowId: flow.id, instruction });
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const rowAction = (d: (typeof diagnostics)[number]) => {
|
|
355
|
+
switch (d.code) {
|
|
356
|
+
case 'missing-param-default':
|
|
357
|
+
return (
|
|
358
|
+
<button
|
|
359
|
+
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
360
|
+
title="Adds an empty placeholder under the Input node's defaults.params — the same fix doctor --fix applies. You can put a real value there later."
|
|
361
|
+
onClick={() => seedParamDefault(d.param!)}
|
|
362
|
+
>
|
|
363
|
+
Seed default
|
|
364
|
+
</button>
|
|
365
|
+
);
|
|
366
|
+
case 'param-no-real-scenario':
|
|
367
|
+
return (
|
|
368
|
+
<button
|
|
369
|
+
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
370
|
+
title="Asks the agent to add a scenario with a real value for this param."
|
|
371
|
+
disabled={agentRunning}
|
|
372
|
+
onClick={() =>
|
|
373
|
+
fixWithAi(
|
|
374
|
+
'new-scenario',
|
|
375
|
+
`Add a scenario that supplies a real value for path param ":${d.param}" (pull a real id from the project's data if you can) and give it an expect.`,
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
>
|
|
379
|
+
Fix with AI
|
|
380
|
+
</button>
|
|
381
|
+
);
|
|
382
|
+
case 'no-expects':
|
|
383
|
+
return (
|
|
384
|
+
<button
|
|
385
|
+
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
386
|
+
title="Asks the agent to write a branch-covering scenario suite with expects."
|
|
387
|
+
disabled={agentRunning}
|
|
388
|
+
onClick={() =>
|
|
389
|
+
fixWithAi(
|
|
390
|
+
'cover-operation',
|
|
391
|
+
'Cover this operation with a branch-covering scenario suite; every scenario gets an expect.',
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
>
|
|
395
|
+
Fix with AI
|
|
396
|
+
</button>
|
|
397
|
+
);
|
|
398
|
+
case 'missing-node-mock':
|
|
399
|
+
return (
|
|
400
|
+
<button
|
|
401
|
+
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
402
|
+
title="Asks the agent to write mocks for this operation's infrastructure nodes."
|
|
403
|
+
disabled={agentRunning}
|
|
404
|
+
onClick={() =>
|
|
405
|
+
fixWithAi(
|
|
406
|
+
'cover-operation',
|
|
407
|
+
`Add realistic mocks for every infrastructure node in this operation${d.nodeId ? ` (including "${d.nodeId}")` : ''}: give the op-level "mocks" map (and each scenario's) a canned output matching the shape each node's implementation returns, so mock runs execute without touching anything real.`,
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
>
|
|
411
|
+
Fix with AI
|
|
412
|
+
</button>
|
|
413
|
+
);
|
|
414
|
+
default:
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
421
|
+
<PopoverTrigger asChild>
|
|
422
|
+
<button
|
|
423
|
+
className="flex h-7 cursor-pointer items-center gap-1 rounded-md px-1.5 text-[11.5px] font-medium text-warn transition-colors hover:bg-warn/10"
|
|
424
|
+
title="This operation has problems — click for details"
|
|
425
|
+
aria-label={`${diagnostics.length} operation problems`}
|
|
426
|
+
>
|
|
427
|
+
<TriangleAlertIcon className="size-3.5" />
|
|
428
|
+
{diagnostics.length}
|
|
429
|
+
</button>
|
|
430
|
+
</PopoverTrigger>
|
|
431
|
+
<PopoverContent className="w-96 max-w-[calc(100vw-2rem)] p-1" align="end">
|
|
432
|
+
<div className="px-2.5 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
|
|
433
|
+
Operation problems
|
|
434
|
+
</div>
|
|
435
|
+
<div className="flex flex-col gap-0.5">
|
|
436
|
+
{diagnostics.map((d, i) => (
|
|
437
|
+
<div key={`${d.code}-${d.param ?? i}`} className="flex items-start gap-2 rounded-sm px-2.5 py-1.5">
|
|
438
|
+
<span className={cn('mt-[5px] size-1.5 shrink-0 rounded-full', severityDot[d.severity])} />
|
|
439
|
+
<span className="flex-1 text-[12px] leading-snug text-foreground/90">{d.message}</span>
|
|
440
|
+
{rowAction(d)}
|
|
441
|
+
</div>
|
|
442
|
+
))}
|
|
443
|
+
</div>
|
|
444
|
+
{diagnostics.length >= 2 && (
|
|
445
|
+
<div className="mt-0.5 border-t border-border/60 px-2.5 pb-1.5 pt-2">
|
|
446
|
+
<button
|
|
447
|
+
className="w-full rounded-sm py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
448
|
+
disabled={agentRunning}
|
|
449
|
+
onClick={() =>
|
|
450
|
+
fixWithAi(
|
|
451
|
+
'cover-operation',
|
|
452
|
+
'Run doctor on this operation and resolve every finding: seed or set param defaults, add scenarios with real param values, and give scenarios expects. Verify with test.',
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
>
|
|
456
|
+
Fix all with AI
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
460
|
+
</PopoverContent>
|
|
461
|
+
</Popover>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* VS Code-style layout toggles: show/hide the sidebar, bottom dock and
|
|
467
|
+
* inspector, plus a terminal button that dismisses/reopens the run console —
|
|
468
|
+
* only present while a watched run's console is available.
|
|
469
|
+
*/
|
|
470
|
+
function LayoutToggles() {
|
|
471
|
+
const dismissRunConsole = useBuilderStore((s) => s.dismissRunConsole);
|
|
472
|
+
const reopenRunConsole = useBuilderStore((s) => s.reopenRunConsole);
|
|
473
|
+
const runConsole = useRunConsole();
|
|
474
|
+
|
|
475
|
+
// Panel visibility lives on the panels themselves (each has a close, closed
|
|
476
|
+
// edges grow a reopen tab); only the contextual run-console toggle stays here.
|
|
477
|
+
// The button occupies its slot even when unavailable (invisible) so a run
|
|
478
|
+
// beginning never reflows the toolbar and shoves the Run/Step controls.
|
|
479
|
+
return (
|
|
480
|
+
<div className="flex items-center gap-0.5">
|
|
481
|
+
<Button
|
|
482
|
+
variant="ghost"
|
|
483
|
+
size="icon"
|
|
484
|
+
className={cn(
|
|
485
|
+
'relative size-7',
|
|
486
|
+
!runConsole.available && 'pointer-events-none invisible',
|
|
487
|
+
runConsole.open ? 'bg-secondary/60 text-foreground' : 'text-muted-foreground',
|
|
488
|
+
)}
|
|
489
|
+
onClick={() => (runConsole.open ? dismissRunConsole() : reopenRunConsole())}
|
|
490
|
+
title="Toggle run console"
|
|
491
|
+
aria-label="Toggle run console"
|
|
492
|
+
tabIndex={runConsole.available ? 0 : -1}
|
|
493
|
+
>
|
|
494
|
+
<TerminalSquareIcon />
|
|
495
|
+
{runConsole.running && (
|
|
496
|
+
<span className="absolute right-1 top-1 size-1.5 rounded-full bg-highlight" />
|
|
497
|
+
)}
|
|
498
|
+
</Button>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function Toolbar() {
|
|
504
|
+
useRunnerPolling();
|
|
505
|
+
const flowName = useBuilderStore((s) => s.flow.name);
|
|
506
|
+
const run = useBuilderStore((s) => s.run);
|
|
507
|
+
const renameFlow = useBuilderStore((s) => s.renameFlow);
|
|
508
|
+
const runToEnd = useBuilderStore((s) => s.runToEnd);
|
|
509
|
+
const stepRun = useBuilderStore((s) => s.stepRun);
|
|
510
|
+
const stepMode = useBuilderStore((s) => s.stepMode);
|
|
511
|
+
const stepping = stepMode && run?.status === 'running';
|
|
512
|
+
const runnerMock = useBuilderStore((s) => s.runnerMock);
|
|
513
|
+
const saveFlow = useBuilderStore((s) => s.saveFlow);
|
|
514
|
+
const exportFlow = useBuilderStore((s) => s.exportFlow);
|
|
515
|
+
const importFlow = useBuilderStore((s) => s.importFlow);
|
|
516
|
+
const executionMode = useBuilderStore((s) => s.executionMode);
|
|
517
|
+
const runnerOnline = useBuilderStore((s) => s.runnerOnline);
|
|
518
|
+
const environments = useBuilderStore((s) => s.environments);
|
|
519
|
+
const selectedEnvironment = useBuilderStore((s) => s.selectedEnvironment);
|
|
520
|
+
const safeMode = useBuilderStore((s) => s.safeMode);
|
|
521
|
+
const agentPanelOpen = useBuilderStore((s) => s.agentPanelOpen);
|
|
522
|
+
const toggleAgentPanel = useBuilderStore((s) => s.toggleAgentPanel);
|
|
523
|
+
const [busy, setBusy] = useState(false);
|
|
524
|
+
const fileRef = useRef<HTMLInputElement>(null);
|
|
525
|
+
const [moreOpen, setMoreOpen] = useState(false);
|
|
526
|
+
|
|
527
|
+
// Only a manual server override with the runner down blocks running;
|
|
528
|
+
// auto mode always has the browser fallback.
|
|
529
|
+
const runDisabled = busy || (executionMode === 'server' && runnerOnline === false);
|
|
530
|
+
const currentEnv = environments.find((e) => e.name === selectedEnvironment);
|
|
531
|
+
const live = (currentEnv?.protected ?? false) && !safeMode;
|
|
532
|
+
|
|
533
|
+
const guard = (work: () => Promise<void>) => async () => {
|
|
534
|
+
setBusy(true);
|
|
535
|
+
try {
|
|
536
|
+
await work();
|
|
537
|
+
} finally {
|
|
538
|
+
setBusy(false);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const handleExport = () => {
|
|
543
|
+
const blob = new Blob([exportFlow()], { type: 'application/json' });
|
|
544
|
+
const url = URL.createObjectURL(blob);
|
|
545
|
+
const anchor = document.createElement('a');
|
|
546
|
+
anchor.href = url;
|
|
547
|
+
anchor.download = `${flowName || 'flow'}.json`;
|
|
548
|
+
anchor.click();
|
|
549
|
+
URL.revokeObjectURL(url);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const handleImportFile = async (file: File | undefined) => {
|
|
553
|
+
if (!file) return;
|
|
554
|
+
try {
|
|
555
|
+
importFlow(await file.text());
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.error('Import failed', err);
|
|
558
|
+
}
|
|
559
|
+
if (fileRef.current) fileRef.current.value = '';
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<header className="flex h-12 items-center gap-3 border-b border-border bg-background px-3">
|
|
564
|
+
<span className="flex items-center gap-1.5 pl-1 text-[13px] font-semibold tracking-tight">
|
|
565
|
+
<FlameIcon className="size-4 text-highlight" /> emberflow
|
|
566
|
+
</span>
|
|
567
|
+
<input
|
|
568
|
+
className="h-8 min-w-44 rounded-md border border-transparent bg-transparent px-2.5 text-[13px] text-muted-foreground outline-none transition-colors hover:border-border focus:border-ring focus:text-foreground focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
569
|
+
value={flowName}
|
|
570
|
+
onChange={(e) => renameFlow(e.target.value)}
|
|
571
|
+
aria-label="Flow name"
|
|
572
|
+
spellCheck={false}
|
|
573
|
+
/>
|
|
574
|
+
<div className="ml-auto flex items-center gap-2">
|
|
575
|
+
<ProblemsChip />
|
|
576
|
+
<EnvironmentPicker />
|
|
577
|
+
<div className="h-5 w-px bg-border" />
|
|
578
|
+
{/* A stepped run in flight keeps the SAME two controls in the SAME
|
|
579
|
+
slots so nothing shifts under a repeated click: the split button's
|
|
580
|
+
main action turns into "Finish" (run to end), and Step becomes the
|
|
581
|
+
highlighted "Step over" primary. */}
|
|
582
|
+
<RunSplitButton
|
|
583
|
+
onRun={guard(runToEnd)}
|
|
584
|
+
runDisabled={runDisabled}
|
|
585
|
+
live={live}
|
|
586
|
+
stepping={stepping}
|
|
587
|
+
/>
|
|
588
|
+
<Button
|
|
589
|
+
variant={stepping ? 'default' : 'secondary'}
|
|
590
|
+
size="sm"
|
|
591
|
+
onClick={guard(stepRun)}
|
|
592
|
+
disabled={runDisabled}
|
|
593
|
+
title={
|
|
594
|
+
stepping
|
|
595
|
+
? 'Runs the next node, then pauses again.'
|
|
596
|
+
: runnerMock
|
|
597
|
+
? 'Runs one node at a time against scenario mocks — real logic, canned infrastructure, paused between nodes.'
|
|
598
|
+
: `Runs one node at a time against "${selectedEnvironment || 'browser'}" — same real execution, paused between nodes.`
|
|
599
|
+
}
|
|
600
|
+
>
|
|
601
|
+
{stepping ? <PlayIcon /> : <StepForwardIcon />}
|
|
602
|
+
{stepping ? 'Step over' : 'Step'}
|
|
603
|
+
</Button>
|
|
604
|
+
<div className="h-5 w-px bg-border" />
|
|
605
|
+
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
|
606
|
+
<PopoverTrigger asChild>
|
|
607
|
+
<Button variant="ghost" size="icon" aria-label="More actions" className="text-muted-foreground hover:text-foreground">
|
|
608
|
+
<EllipsisIcon />
|
|
609
|
+
</Button>
|
|
610
|
+
</PopoverTrigger>
|
|
611
|
+
<PopoverContent className="w-44 p-1" align="end">
|
|
612
|
+
{[
|
|
613
|
+
{ icon: <SaveIcon className="size-3.5" />, label: 'Save', action: () => saveFlow() },
|
|
614
|
+
{ icon: <DownloadIcon className="size-3.5" />, label: 'Export', action: handleExport },
|
|
615
|
+
{ icon: <UploadIcon className="size-3.5" />, label: 'Import', action: () => fileRef.current?.click() },
|
|
616
|
+
].map(({ icon, label, action }) => (
|
|
617
|
+
<button
|
|
618
|
+
key={label}
|
|
619
|
+
type="button"
|
|
620
|
+
onClick={() => {
|
|
621
|
+
action();
|
|
622
|
+
setMoreOpen(false);
|
|
623
|
+
}}
|
|
624
|
+
className="flex w-full cursor-pointer items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-left text-[12.5px] text-foreground transition-colors hover:bg-accent"
|
|
625
|
+
>
|
|
626
|
+
<span className="text-muted-foreground">{icon}</span>
|
|
627
|
+
{label}
|
|
628
|
+
</button>
|
|
629
|
+
))}
|
|
630
|
+
</PopoverContent>
|
|
631
|
+
</Popover>
|
|
632
|
+
<input
|
|
633
|
+
ref={fileRef}
|
|
634
|
+
type="file"
|
|
635
|
+
accept="application/json,.json"
|
|
636
|
+
hidden
|
|
637
|
+
onChange={(e) => handleImportFile(e.target.files?.[0])}
|
|
638
|
+
/>
|
|
639
|
+
<div className="h-5 w-px bg-border" />
|
|
640
|
+
<LayoutToggles />
|
|
641
|
+
{/* Setup checklist entry point lives in the StatusBar (bottom) — the
|
|
642
|
+
dialog itself stays mounted here for auto-open on fresh projects. */}
|
|
643
|
+
<WelcomeDialog />
|
|
644
|
+
<SettingsDialog />
|
|
645
|
+
<Button
|
|
646
|
+
variant="ghost"
|
|
647
|
+
size="icon"
|
|
648
|
+
onClick={toggleAgentPanel}
|
|
649
|
+
aria-pressed={agentPanelOpen}
|
|
650
|
+
aria-label="Toggle agent panel"
|
|
651
|
+
title="Agent — chat to change this operation, watch it update live"
|
|
652
|
+
className={cn(
|
|
653
|
+
agentPanelOpen
|
|
654
|
+
? 'bg-secondary text-foreground'
|
|
655
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
656
|
+
)}
|
|
657
|
+
>
|
|
658
|
+
<SparklesIcon />
|
|
659
|
+
</Button>
|
|
660
|
+
</div>
|
|
661
|
+
</header>
|
|
662
|
+
);
|
|
663
|
+
}
|