mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 08:02:19 +00:00
feat(aichat): simplify flow mode edits (#6981)
* draft
* Phase 1: Remove deprecated granular flow AI tools
Simplify AI chat flow mode to use only YAML-based editing:
- Remove all commented-out granular tools (add_step, remove_step, set_code, etc.)
- Clean up FlowAIChatHelpers interface to only essential methods
- Update system prompts to focus on YAML-only workflow
- Remove unused imports and type definitions
This is part of a larger refactoring to simplify the flow editing
experience to a single YAML editing tool with automatic diff visualization.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* use minified json
* use openflow in system prompt
* handle inputs
* cleaning
* cleaning
* diffmode in flowgraph
* remove acceptrejectmodule
* use new diff mode
* cleaning
* better props
* better logic
* cleaning
* accept reject logic
* use get set
* draft manager
* use diff manager
* draft
* Refactor flowDiffManager to be instance-based with auto-computation
- Remove singleton export, making it instantiable per FlowGraphV2
- Add afterFlow state tracking for auto-diff computation
- Add beforeInputSchema/afterInputSchema for schema change tracking
- Add $effect for reactive auto-computation when beforeFlow/afterFlow changes
- Add setAfterFlow() and setInputSchemas() methods
- Simplify accept/reject methods to just mark pending=false
- Add validation to throw error when accepting/rejecting without beforeFlow
- Update setSnapshot to accept undefined for clearing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor FlowGraphV2 to own diffManager instance
- Import and create diffManager instance per FlowGraphV2
- Remove onAcceptModule and onRejectModule props
- Add validation $effect to error if both diffBeforeFlow and moduleActions provided
- Add $effect to sync props (diffBeforeFlow or moduleActions) to diffManager
- Add $effect to watch current flow changes and update afterFlow
- Replace computedDiff with diffManager.moduleActions
- Use raw modules instead of merged flow (diffManager handles merging)
- Expose getDiffManager() and setBeforeFlow() methods
- Pass diffManager to graph context instead of callbacks
- Remove $inspect for removed props
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update FlowModuleSchemaMap to use FlowGraphV2's diffManager
- Remove import of flowDiffManager singleton
- Update setBeforeFlow to call graph.setBeforeFlow()
- Update setModuleActions and getModuleActions to use graph.getDiffManager()
- Add getDiffManager() proxy method
- Simplify handleAcceptModule and handleRejectModule to use new API
- Handle editor state separately from diff operations
- Remove diffBeforeFlow, moduleActions, onAcceptModule, onRejectModule props passed to FlowGraphV2
- Remove onAcceptModule and onRejectModule from Props interface and destructured props
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update FlowAIChat to use flowModuleSchemaMap's diffManager
- Remove import of flowDiffManager singleton
- Update revertToSnapshot to use flowModuleSchemaMap.getDiffManager()
- Add null check for diffManager before using
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Verify FlowGraphDiffViewer compatibility with refactored architecture
FlowGraphDiffViewer already uses the correct prop patterns:
- Before graph: moduleActions prop (display-only mode)
- After graph: diffBeforeFlow prop (full diff mode with auto-computation)
Each FlowGraphV2 instance creates its own diffManager, making the side-by-side
view work correctly with independent diff state per graph.
No code changes required.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update graph components to use diffManager instead of callbacks
- Update graphBuilder.svelte.ts to pass diffManager instead of onAcceptModule/onRejectModule
- Update InputNode and ModuleN type definitions with diffManager
- Update ModuleNode.svelte to pass diffManager to MapItem
- Update MapItem.svelte to pass diffManager to FlowModuleSchemaItem
- Update FlowModuleSchemaItem.svelte to use diffManager directly for accept/reject
- Replace callback-based accept/reject with direct diffManager calls
- Only show accept/reject buttons when beforeFlow exists and action is pending
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix removed modules not showing in diff viewer
Problem: After refactoring, removed modules were no longer appearing in the
diff viewer because we changed effectiveModules from using the merged flow
(which includes removed modules) to using raw modules.
Solution:
- Add mergedFlow state to flowDiffManager to store timeline's merged flow
- Add markRemovedAsShadowed parameter support for side-by-side view
- Store timeline.mergedFlow in auto-computation $effect
- Add getter for mergedFlow and setMarkRemovedAsShadowed method
- Clear mergedFlow in clearSnapshot()
- Update FlowGraphV2 to set markRemovedAsShadowed in diffManager
- Update effectiveModules/FailureModule/PreprocessorModule to use mergedFlow
The merged flow contains all modules including removed ones, enabling:
- Unified view: Removed modules appear in red with "removed" badge
- Side-by-side view: Removed modules show as shadowed in After graph
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify accept/reject logic by removing actions instead of toggling pending state
Previously, accepting or rejecting a module action would set pending to false but keep the action in the moduleActions map. This caused a bug where the $effect would overwrite moduleActions with fresh actions having pending: true, making accept/reject buttons reappear on previously handled modules.
Now, when a user accepts or rejects a module action, we remove it entirely from the moduleActions map. This is simpler and fixes the button reappearing issue.
Changes:
- acceptModule: Remove action from moduleActions instead of setting pending: false
- rejectModule: Remove action from moduleActions instead of setting pending: false
- checkAndClearSnapshot: Check if moduleActions is empty instead of checking pending states
- Fix typo: getModuleFromFrom → getModuleFromFlow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* cleaning
* fix logic
* make diff drawer part of manager
* accept submodules
* fixes
* Phase 4: Add checkAndApplyChanges() helper to flowDiffManager
- Added new checkAndApplyChanges() function to apply mergedFlow to flowStore when all changes are decided
- This replaces the old checkAndClearSnapshot() behavior and ensures flowStore is updated atomically
- Handles both flow structure and input schema updates
* Phase 2: Simplify acceptModule() - only modify mergedFlow
- Remove flowStore mutations from acceptModule()
- For removed modules: just delete the shadowed (__prefix) version from mergedFlow
- For added/modified: no action needed (already correct in mergedFlow)
- Call checkAndApplyChanges() to apply changes when all decided
* Phase 3: Simplify rejectModule() - only modify mergedFlow
- Remove all flowStore mutations from rejectModule()
- For added modules: delete from mergedFlow
- For removed modules: replace shadowed (__) module with original from beforeFlow
- For modified modules: restore old version in mergedFlow
- For Input schema: revert afterInputSchema
- Call checkAndApplyChanges() to apply changes when all decided
* Phase 5: Verify acceptAll/rejectAll work with new architecture
- acceptAll() and rejectAll() already pass options correctly to acceptModule/rejectModule
- They will automatically benefit from checkAndApplyChanges()
- No changes needed for this phase
* Phase 6: Remove FlowGraphV2 reactive effect that updates afterFlow
- Removed the (lines 252-266) that continuously updated afterFlow
- This effect created reactive loops when flowStore changed
- afterFlow should only be set once when AI generates changes via setFlowYaml()
- The initial sync effect (lines 226-250) is kept for prop-driven diff mode
* Phase 7: Update FlowAIChat setFlowYaml to use diffManager
- Changed setFlowYaml() to use diffManager.setAfterFlow() instead of modifying flowStore
- flowStore remains unchanged during AI review phase
- Changes are staged in mergedFlow for user review
- Only applied to flowStore when all changes are accepted/rejected
- Added error handling for missing diffManager
* Fix linter warnings
- Remove unused FlowTimeline type import
- Fix ChangeTracker initialization with proper type parameter
- Keep deleteModuleFromFlow and checkAndClearSnapshot for potential future use
* Update plan document with implementation status
- Mark all phases as complete
- Add commit references
- Update file checklist
- Add implementation summary at top of document
* Add comprehensive implementation summary document
- Detailed overview of architecture changes
- Before/after comparisons for each file
- Complete testing scenarios checklist
- Troubleshooting guide
- Migration notes and backwards compatibility info
* Show pending modules in editor panel
- Pass diffManager from FlowModuleSchemaMap to FlowEditorPanel
- Add effectiveModules derived value that uses mergedFlow when in diff mode
- Update module iteration to use effectiveModules instead of flowStore
- Allows users to view added/modified modules during AI review
- Fixes issue where clicking on pending modules showed nothing
* Add implementation summary for show pending modules feature
* fix
* shorter system prompt
* Fix Input schema diff mode issues
- Add Accept/Reject buttons to Input node (previously only showed Diff button)
- Pass diffManager to FlowInput component
- Add effectiveSchema derived value that uses afterInputSchema when in diff mode
- Add effectiveDisabled to prevent editing Input when reviewing AI changes
- Update FlowInputViewer to show pending schema changes
- Fixes issue where Input schema changes couldn't be accepted/rejected
- Fixes issue where pending Input schema wasn't visible in the panel
* Disable delete and move buttons when in pending mode
- Add effectiveDeletable derived value that checks diffManager.hasPendingChanges
- Replace all instances of deletable with effectiveDeletable in template
- Prevents delete/move operations when AI changes are being reviewed
- Delete and move buttons are hidden when there are pending changes
- Buttons reappear once all changes are accepted or rejected
- Prevents conflicting operations during review phase
* no move or delte when reviewing
* use context
* inline script reduction
* use json
* rollback to direct modif
* fix merge
* cleaning
* fix reject removed
* add set step code tool
* better prompt
* add back relevant tools
* add back accept reject
* use edit mode for pending
* fix input
* remove unneeded effect
* cleaner + bug fix
* fix failure and preprocessor
* fix show diff for failure module
* fix accept reject on failre module
* no auto add module to context
* cleaning
* add back effect
* cleaning
* fix multiple setflowjson
* track effectivemoduleactions for graph rendering
* nit prompt
* styling
* rm md files
* rm flake copy
* cleaning
* fix z index
* fix revert
* only change before after
* use add remove modify tools
* input + failure + preproc tools
* parsing issues
* nit
* use raw schema for tools
* resolve ref for gemini
* fix schema
* show test on graph
* much cleaner logic
* ignore empty assets
* Remove debug console.log statements from production code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove debug $inspect calls from FlowGraphV2
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add error logging to setFlowJson before re-throwing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Standardize null/undefined handling to prefer null
- Use .nullable().optional() instead of .nullish() in Zod schemas
- Simplify addModuleToFlow signature to use string | null
- Coerce undefined to null when extracting parsed args
- Simplify null checks to only check !== null
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove debug console.log from AI tool functions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Extract special module IDs to constants
Add SPECIAL_MODULE_IDS constant with INPUT, PREPROCESSOR, and FAILURE
to avoid magic strings throughout the flow AI chat code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add cleanup for diffDrawer reference on unmount
Prevents potential memory leaks by clearing the diffDrawer reference
when the FlowGraphV2 component is destroyed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use structuredClone instead of JSON.parse(JSON.stringify())
structuredClone is more efficient and type-safe for deep cloning objects.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Cache module lookups in reconstructMergedFlow
Move getAllModulesMap and getAllModuleIds calls outside the loop to avoid
redundant recomputation. Track merged IDs incrementally as modules are added.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert "Use structuredClone instead of JSON.parse(JSON.stringify())"
This reverts commit a62ba5b980.
* cleaning
* allow delete
* better openflow for ai agents + truncate system prompt
* handle ai agent tools
* fix set code for tool
* fix wrong cancel request called
* mark tool calls as canceled
* get lang instructions
* use streamiing args
* give db url to claude
* fix revert
* save and clear when leaving editor
* keep whitespace in user message
* uniformize colors
* fix diff button
* remove db from backend claude
* remove move module tool
* no failure and preprocessor
* fix error given to llm
* fix z index
* fix ts errors
* cleaning
* fix add module logic
* fix(copilot): add 'tools' to branchPath description for aiagent containers
The branchPath parameter description was missing 'tools' option for aiagent
containers and didn't mention branchall support.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): correct AI agent tool IDs and summaries documentation
Tool summaries CAN contain spaces (they're human-readable descriptions).
Only tool IDs must avoid spaces.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): remove reference to non-existent set_flow_json tool
The set_module_code tool description referenced set_flow_json which
doesn't exist as an exposed tool (it's an internal helper).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): clarify inspect_inline_script is read-only
The tool description incorrectly suggested it could modify code.
This tool only inspects - use set_module_code to modify.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): clarify afterId behavior for AI agent tools
Updated wording to clarify that afterId can be used but is optional
for AI agent tools since tool order doesn't affect execution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(copilot): remove unused id param from get_instructions_for_code_generation
The id parameter was only used to check for preprocessor, which is no
longer needed. Simplified the tool to only require the language param.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(copilot): add result format to search_scripts tool description
Helps AI understand what data format to expect from the tool.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(copilot): add result format to resource_type tool description
Helps AI understand what data format to expect from the tool and
provides example resource type names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* nit
* Add support for adding branches to branchall/branchone via add_module
Previously, add_module could only add modules inside existing branches.
Now, using insideId with branchPath=null will add a NEW branch to a
branchall or branchone container.
API:
- add_module({ insideId: "my_branchall", branchPath: null, value: { summary: "New Branch", skip_failure: false, modules: [] } })
- add_module({ insideId: "my_branchone", branchPath: null, value: { summary: "Condition", expr: "...", modules: [] } })
Changes:
- Extended addModuleToFlow to handle branchPath=null case
- Updated validation to allow branchPath=null when adding branches
- Updated tool descriptions and system prompt documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* nit
* add remove branch tool
* check all ids for duplicates
* no dup
* nit
* cleaning
* fix dup ids
* split core.ts
* only mount diff drawer if useful
* remove wrong logic
* update exprs
* fix
* chore(flow): Add unit tests to flow diff manager (#7291)
* setup
* add basic tests for flowdiff
* add complex tests
* fix branch issue
* more complex tests
* add flow diff manager tests
* add utils
* better handling of moved case
* more tests for move case
* add buggy test case
* rm
* rework config
* cleaning
* fix config
* rm
* fix for reverting type change module
* all good
* rm
* add missing testmode
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,3 +19,17 @@ When implementing new features in Windmill, follow these best practices:
|
||||
|
||||
- Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt
|
||||
- Frontend (Svelte 5): @frontend/svelte5-best-practices.mdc
|
||||
|
||||
## Querying the Database
|
||||
|
||||
To query the database directly, use psql with the following connection string:
|
||||
|
||||
```bash
|
||||
psql postgres://postgres:changeme@localhost:5432/windmill
|
||||
```
|
||||
|
||||
This can be helpful for:
|
||||
|
||||
- Inspecting database state during development
|
||||
- Testing queries before implementing them in Rust
|
||||
- Debugging data-related issues
|
||||
|
||||
@@ -10,16 +10,3 @@
|
||||
|
||||
1. Update database schema with migration if necessary
|
||||
2. Update backend/windmill-api/openapi.yaml after modifying API endpoints
|
||||
|
||||
## Querying the Database
|
||||
|
||||
To query the database directly, use psql with the following connection string:
|
||||
|
||||
```bash
|
||||
psql postgres://postgres:changeme@localhost:5432/windmill
|
||||
```
|
||||
|
||||
This can be helpful for:
|
||||
- Inspecting database state during development
|
||||
- Testing queries before implementing them in Rust
|
||||
- Debugging data-related issues
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Script to generate minified OpenFlow JSON for frontend AI system prompt
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source_file="${script_dirpath}/../windmill-yaml-validator/src/gen/openflow.json"
|
||||
output_dirpath="${script_dirpath}/src/lib/components/copilot/chat/flow"
|
||||
output_file="${output_dirpath}/openFlow.json"
|
||||
|
||||
echo "Generating minified OpenFlow JSON..."
|
||||
|
||||
# Validate source file exists
|
||||
if [ ! -f "${source_file}" ]; then
|
||||
echo "Error: Source file not found: ${source_file}"
|
||||
echo "Please run windmill-yaml-validator/gen_openflow_schema.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "${output_dirpath}"
|
||||
|
||||
# Minify JSON by removing all whitespace
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const sourceFile = '${source_file}';
|
||||
const outputFile = '${output_file}';
|
||||
|
||||
try {
|
||||
const schema = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
|
||||
|
||||
// Minify: stringify without spaces
|
||||
const minified = JSON.stringify(schema);
|
||||
|
||||
fs.writeFileSync(outputFile, minified);
|
||||
|
||||
const originalSize = fs.statSync(sourceFile).size;
|
||||
const minifiedSize = fs.statSync(outputFile).size;
|
||||
const savings = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
|
||||
|
||||
console.log(' Minified OpenFlow JSON generated successfully');
|
||||
console.log(' Original size: ' + (originalSize / 1024).toFixed(1) + ' KB');
|
||||
console.log(' Minified size: ' + (minifiedSize / 1024).toFixed(1) + ' KB');
|
||||
console.log(' Savings: ' + savings + '%');
|
||||
console.log(' Output: ' + outputFile);
|
||||
} catch (e) {
|
||||
console.error('Error minifying JSON:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
"
|
||||
|
||||
echo "Done!"
|
||||
Generated
+521
-10
@@ -114,6 +114,7 @@
|
||||
"@types/vscode": "^1.83.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"@vitest/browser-playwright": "^4.0.10",
|
||||
"@zerodevx/svelte-toast": "^0.9.6",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^6.0.1",
|
||||
@@ -123,6 +124,7 @@
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"playwright": "^1.56.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.1.0",
|
||||
@@ -145,6 +147,8 @@
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "npm:rolldown-vite@7.2.8",
|
||||
"vite-plugin-mkcert": "^1.17.5",
|
||||
"vitest": "^4.0.10",
|
||||
"vitest-browser-svelte": "^2.0.1",
|
||||
"yootils": "^0.3.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -2481,6 +2485,53 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test/node_modules/playwright": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
|
||||
"integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test/node_modules/playwright-core": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz",
|
||||
"integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
@@ -3046,6 +3097,17 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
@@ -3340,6 +3402,13 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/diff": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
|
||||
@@ -3649,6 +3718,178 @@
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.15.tgz",
|
||||
"integrity": "sha512-zedtczX688KehaIaAv7m25CeDLb0gBtAOa2Oi1G1cqvSO5aLSVfH6lpZMJLW8BKYuWMxLQc9/5GYoM+jgvGIrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/mocker": "4.0.15",
|
||||
"@vitest/utils": "4.0.15",
|
||||
"magic-string": "^0.30.21",
|
||||
"pixelmatch": "7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.0.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser-playwright": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.15.tgz",
|
||||
"integrity": "sha512-94yVpDbb+ykiT7mK6ToonGnq2GIHEQGBTZTAzGxBGQXcVNCh54YKC2/WkfaDzxy0m6Kgw05kq3FYHKHu+wRdIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.0.15",
|
||||
"@vitest/mocker": "4.0.15",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "4.0.15"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz",
|
||||
"integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.15",
|
||||
"@vitest/utils": "4.0.15",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
|
||||
"integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.15",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz",
|
||||
"integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz",
|
||||
"integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.15",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz",
|
||||
"integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.15",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz",
|
||||
"integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz",
|
||||
"integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.15",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/iconv-lite-umd": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz",
|
||||
@@ -3958,6 +4199,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
@@ -4497,6 +4748,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
|
||||
"integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -5717,6 +5978,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
@@ -6197,6 +6465,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||
"integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -8304,9 +8582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
@@ -9766,6 +10044,17 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.6.tgz",
|
||||
@@ -10188,6 +10477,19 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
|
||||
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pngjs": "^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
@@ -10208,13 +10510,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
|
||||
"integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
|
||||
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.0"
|
||||
"playwright-core": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -10227,9 +10529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.56.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz",
|
||||
"integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -10263,6 +10565,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -11842,6 +12154,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -12044,6 +12363,20 @@
|
||||
"license": "CC0-1.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -13045,6 +13378,13 @@
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
@@ -13100,6 +13440,16 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -13652,6 +14002,128 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz",
|
||||
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.15",
|
||||
"@vitest/mocker": "4.0.15",
|
||||
"@vitest/pretty-format": "4.0.15",
|
||||
"@vitest/runner": "4.0.15",
|
||||
"@vitest/snapshot": "4.0.15",
|
||||
"@vitest/spy": "4.0.15",
|
||||
"@vitest/utils": "4.0.15",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.15",
|
||||
"@vitest/browser-preview": "4.0.15",
|
||||
"@vitest/browser-webdriverio": "4.0.15",
|
||||
"@vitest/ui": "4.0.15",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-browser-svelte": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest-browser-svelte/-/vitest-browser-svelte-2.0.1.tgz",
|
||||
"integrity": "sha512-z7GFio7vxaOolY+xwPUMEKuwL4KcPzB8+bepA9F0Phqag/TJ4j7IAGSwm4Y/FBh7KznsP+7aEIllMay0qDpFXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode": {
|
||||
"name": "@codingame/monaco-vscode-extension-api",
|
||||
"version": "21.6.0",
|
||||
@@ -13825,6 +14297,23 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-csharp": {
|
||||
"version": "1.510.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-csharp/-/windmill-parser-wasm-csharp-1.510.1.tgz",
|
||||
@@ -14033,6 +14522,28 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
"generate-backend-client": "openapi-ts --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions --enums javascript --format false",
|
||||
"generate-backend-client-mac": "openapi-ts --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions --enums javascript",
|
||||
"pretest": "tsc --incremental -p tests/tsconfig.json",
|
||||
"test": "playwright test --config=tests-out/playwright.config.js",
|
||||
"filter-classes": "node filterTailwindClasses.js"
|
||||
"test": "playwright test --config=tests-out/playwright.config.js && npm run test:unit -- --run",
|
||||
"filter-classes": "node filterTailwindClasses.js",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/core": "^1.3.1",
|
||||
@@ -37,6 +38,7 @@
|
||||
"@types/vscode": "^1.83.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"@vitest/browser-playwright": "^4.0.10",
|
||||
"@zerodevx/svelte-toast": "^0.9.6",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^6.0.1",
|
||||
@@ -46,6 +48,7 @@
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"playwright": "^1.56.1",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.1.0",
|
||||
@@ -68,6 +71,8 @@
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "npm:rolldown-vite@7.2.8",
|
||||
"vite-plugin-mkcert": "^1.17.5",
|
||||
"vitest": "^4.0.10",
|
||||
"vitest-browser-svelte": "^2.0.1",
|
||||
"yootils": "^0.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
}
|
||||
|
||||
function hasAIChanges(): boolean {
|
||||
return aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
|
||||
return aiChatManager.flowAiChatHelpers?.hasPendingChanges() ?? false
|
||||
}
|
||||
|
||||
function withAIChangesWarning(callback: () => void) {
|
||||
@@ -896,7 +896,7 @@
|
||||
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
|
||||
})
|
||||
$effect.pre(() => {
|
||||
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
|
||||
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasPendingChanges() ?? false
|
||||
customUi && untrack(() => onCustomUiChange(customUi, hasAiDiff))
|
||||
})
|
||||
|
||||
@@ -1123,8 +1123,7 @@
|
||||
)
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
const currentFlow =
|
||||
aiChatManager.flowAiChatHelpers?.getPreviewFlow() ?? flowStore.val
|
||||
const currentFlow = flowStore.val
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: deployedValue ?? savedFlow,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { FlowModule, FlowValue, OpenFlow } from '$lib/gen'
|
||||
import type { OpenFlow } from '$lib/gen'
|
||||
import YAML from 'yaml'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { Alert, Button } from './common'
|
||||
import { buildFlowTimeline, hasInputSchemaChanged } from './flows/flowDiff'
|
||||
import { dfs } from './flows/dfs'
|
||||
import DiffDrawer from './DiffDrawer.svelte'
|
||||
import { computeFlowModuleDiff } from './flows/flowDiff'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
@@ -22,7 +20,6 @@
|
||||
let { beforeYaml, afterYaml }: Props = $props()
|
||||
|
||||
let parseError = $state<string | undefined>(undefined)
|
||||
let moduleDiffDrawer: DiffDrawer | undefined = $state(undefined)
|
||||
let viewerWidth = $state(SIDE_BY_SIDE_MIN_WIDTH)
|
||||
let beforePaneSize = $state(50)
|
||||
let viewMode = $state<'sidebyside' | 'unified'>('sidebyside')
|
||||
@@ -53,38 +50,17 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Detect if input schema has changed
|
||||
let inputSchemaModified = $derived(hasInputSchemaChanged(beforeFlow, afterFlow))
|
||||
|
||||
// Determine if we should render side-by-side or unified (user controlled via toggle)
|
||||
let isSideBySide = $derived(viewMode === 'sidebyside')
|
||||
|
||||
// Build timeline using history-based approach
|
||||
// In side-by-side view, mark removed modules as 'shadowed' in the After graph
|
||||
// In unified view, mark removed modules as 'removed' to show them in red
|
||||
let timeline = $derived.by(() => {
|
||||
if (!beforeFlow || !afterFlow) return undefined
|
||||
return buildFlowTimeline(beforeFlow.value, afterFlow.value, {
|
||||
markRemovedAsShadowed: isSideBySide
|
||||
})
|
||||
const { beforeActions } = $derived.by(() => {
|
||||
if (!beforeFlow || !afterFlow) return { beforeActions: undefined }
|
||||
return computeFlowModuleDiff(beforeFlow.value, afterFlow.value)
|
||||
})
|
||||
|
||||
// Extract merged flow from timeline
|
||||
let mergedFlow = $derived(timeline?.mergedFlow)
|
||||
|
||||
// Get the unified actions directly from timeline
|
||||
let unifiedActions = $derived(timeline?.afterActions ?? {})
|
||||
|
||||
// Helper to find module by ID in a flow
|
||||
function getModuleById(flow: FlowValue, moduleId: string): FlowModule | undefined {
|
||||
const allModules = dfs(flow.modules ?? [], (m) => m)
|
||||
return (
|
||||
allModules.find((m) => m?.id === moduleId) ??
|
||||
(flow.failure_module?.id === moduleId ? flow.failure_module : undefined) ??
|
||||
(flow.preprocessor_module?.id === moduleId ? flow.preprocessor_module : undefined)
|
||||
)
|
||||
}
|
||||
|
||||
// Handler for viewport changes - updates shared state for synchronization
|
||||
function handleViewportChange(viewport: Viewport, isUserInitiated: boolean) {
|
||||
if (isUserInitiated) {
|
||||
@@ -92,36 +68,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Callback to show module diff
|
||||
function handleShowModuleDiff(moduleId: string) {
|
||||
if (!beforeFlow || !afterFlow) return
|
||||
|
||||
// Handle special case for Input schema diff
|
||||
if (moduleId === 'Input') {
|
||||
moduleDiffDrawer?.openDrawer()
|
||||
moduleDiffDrawer?.setDiff({
|
||||
mode: 'simple',
|
||||
title: 'Flow Input Schema Diff',
|
||||
original: { schema: beforeFlow.schema ?? {} },
|
||||
current: { schema: afterFlow.schema ?? {} }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const beforeModule = getModuleById(beforeFlow.value, moduleId)
|
||||
const afterModule = getModuleById(afterFlow.value, moduleId)
|
||||
|
||||
if (beforeModule && afterModule) {
|
||||
moduleDiffDrawer?.openDrawer()
|
||||
moduleDiffDrawer?.setDiff({
|
||||
mode: 'simple',
|
||||
title: `Module Diff: ${moduleId}`,
|
||||
original: beforeModule,
|
||||
current: afterModule
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (viewerWidth < SIDE_BY_SIDE_MIN_WIDTH) {
|
||||
viewMode = 'unified'
|
||||
@@ -198,9 +144,7 @@
|
||||
preprocessorModule={beforeFlow.value.preprocessor_module}
|
||||
earlyStop={beforeFlow.value.skip_expr !== undefined}
|
||||
cache={beforeFlow.value.cache_ttl !== undefined}
|
||||
moduleActions={timeline?.beforeActions}
|
||||
{inputSchemaModified}
|
||||
onShowModuleDiff={handleShowModuleDiff}
|
||||
moduleActions={beforeActions}
|
||||
notSelectable={true}
|
||||
insertable={false}
|
||||
editMode={false}
|
||||
@@ -223,63 +167,56 @@
|
||||
<Pane minSize={30} class="flex flex-col h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
{#if mergedFlow}
|
||||
<FlowGraphV2
|
||||
bind:this={afterGraph}
|
||||
modules={mergedFlow.modules}
|
||||
failureModule={mergedFlow.failure_module}
|
||||
preprocessorModule={mergedFlow.preprocessor_module}
|
||||
earlyStop={mergedFlow.skip_expr !== undefined}
|
||||
cache={mergedFlow.cache_ttl !== undefined}
|
||||
moduleActions={unifiedActions}
|
||||
{inputSchemaModified}
|
||||
onShowModuleDiff={handleShowModuleDiff}
|
||||
notSelectable={true}
|
||||
insertable={false}
|
||||
editMode={false}
|
||||
download={false}
|
||||
scroll={false}
|
||||
minHeight={400}
|
||||
triggerNode={false}
|
||||
{sharedViewport}
|
||||
onViewportChange={handleViewportChange}
|
||||
>
|
||||
{#snippet leftHeader()}
|
||||
<span class="text-sm text-primary">After</span>
|
||||
{/snippet}
|
||||
</FlowGraphV2>
|
||||
{/if}
|
||||
<FlowGraphV2
|
||||
bind:this={afterGraph}
|
||||
diffBeforeFlow={beforeFlow}
|
||||
modules={afterFlow.value.modules}
|
||||
failureModule={afterFlow.value.failure_module}
|
||||
preprocessorModule={afterFlow.value.preprocessor_module}
|
||||
earlyStop={afterFlow.value.skip_expr !== undefined}
|
||||
cache={afterFlow.value.cache_ttl !== undefined}
|
||||
currentInputSchema={afterFlow.schema}
|
||||
markRemovedAsShadowed={true}
|
||||
notSelectable={true}
|
||||
insertable={false}
|
||||
editMode={false}
|
||||
download={false}
|
||||
scroll={false}
|
||||
minHeight={400}
|
||||
triggerNode={false}
|
||||
{sharedViewport}
|
||||
onViewportChange={handleViewportChange}
|
||||
>
|
||||
{#snippet leftHeader()}
|
||||
<span class="text-sm text-primary">After</span>
|
||||
{/snippet}
|
||||
</FlowGraphV2>
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else}
|
||||
<!-- Unified view for narrow screens - show merged flow with all diff colors -->
|
||||
{#if mergedFlow}
|
||||
<div class="h-full overflow-hidden">
|
||||
<FlowGraphV2
|
||||
modules={mergedFlow.modules}
|
||||
failureModule={mergedFlow.failure_module}
|
||||
preprocessorModule={mergedFlow.preprocessor_module}
|
||||
earlyStop={mergedFlow.skip_expr !== undefined}
|
||||
cache={mergedFlow.cache_ttl !== undefined}
|
||||
moduleActions={unifiedActions}
|
||||
{inputSchemaModified}
|
||||
onShowModuleDiff={handleShowModuleDiff}
|
||||
notSelectable={true}
|
||||
insertable={false}
|
||||
editMode={false}
|
||||
download={false}
|
||||
scroll={false}
|
||||
minHeight={400}
|
||||
triggerNode={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Unified view - uses FlowGraphV2's built-in diff mode -->
|
||||
<div class="h-full overflow-hidden">
|
||||
<FlowGraphV2
|
||||
diffBeforeFlow={beforeFlow}
|
||||
modules={afterFlow.value.modules}
|
||||
failureModule={afterFlow.value.failure_module}
|
||||
preprocessorModule={afterFlow.value.preprocessor_module}
|
||||
earlyStop={afterFlow.value.skip_expr !== undefined}
|
||||
cache={afterFlow.value.cache_ttl !== undefined}
|
||||
currentInputSchema={afterFlow.schema}
|
||||
notSelectable={true}
|
||||
insertable={false}
|
||||
editMode={false}
|
||||
download={false}
|
||||
scroll={false}
|
||||
minHeight={400}
|
||||
triggerNode={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Nested DiffDrawer for module-level diffs -->
|
||||
<DiffDrawer bind:this={moduleDiffDrawer} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
|
||||
import type { DurationStatus, GraphModuleState } from './graph'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
|
||||
import FlowChat from './flows/conversations/FlowChat.svelte'
|
||||
|
||||
interface Props {
|
||||
@@ -130,11 +128,11 @@
|
||||
})
|
||||
|
||||
function extractFlow(previewMode: 'upTo' | 'whole'): OpenFlow {
|
||||
const previewFlow = aiChatManager.flowAiChatHelpers?.getPreviewFlow()
|
||||
const previewFlow = flowStore.val
|
||||
if (previewMode === 'whole') {
|
||||
return previewFlow ?? flowStore.val
|
||||
return previewFlow
|
||||
} else {
|
||||
const flow = previewFlow ?? stateSnapshot(flowStore).val
|
||||
const flow = previewFlow
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
let upToIndex = idOrders.indexOf(upToId ?? selectionManager.getSelectedId() ?? '')
|
||||
|
||||
@@ -334,7 +332,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grow justify-center flex flex-row gap-2">
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel && aiChatManager.flowAiChatHelpers?.getModuleAction(selectedJobStep) !== 'removed'}
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel}
|
||||
{#if selectedJobStepType == 'single'}
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ScriptService, type FlowModule, type JavascriptTransform, type Job } from '$lib/gen'
|
||||
import { ScriptService, type AiAgent, type FlowModule, type JavascriptTransform, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { getContext } from 'svelte'
|
||||
@@ -111,7 +111,7 @@
|
||||
value: {
|
||||
type: 'aiagent',
|
||||
tools: mod.value.type == 'aiagent' ? mod.value.tools : [],
|
||||
input_transforms: inputTransforms
|
||||
input_transforms: inputTransforms as AiAgent['input_transforms']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -439,6 +439,7 @@
|
||||
aiChatManager.scriptEditorApplyCode = undefined
|
||||
aiChatManager.scriptEditorShowDiffMode = undefined
|
||||
aiChatManager.scriptEditorOptions = undefined
|
||||
aiChatManager.saveAndClear()
|
||||
aiChatManager.changeMode(AIMode.NAVIGATOR)
|
||||
})
|
||||
|
||||
|
||||
@@ -212,14 +212,14 @@
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
{:else if aiChatManager.flowAiChatHelpers?.hasDiff()}
|
||||
{:else if aiChatManager.flowAiChatHelpers?.hasPendingChanges()}
|
||||
<div class="absolute -top-10 w-full flex flex-row justify-center gap-2">
|
||||
<Button
|
||||
startIcon={{ icon: CheckIcon }}
|
||||
size="xs"
|
||||
variant="default"
|
||||
btnClasses="bg-green-500 hover:bg-green-600 text-white hover:text-white"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
aiChatManager.flowAiChatHelpers?.acceptAllModuleActions()
|
||||
}}
|
||||
>
|
||||
@@ -230,7 +230,7 @@
|
||||
size="xs"
|
||||
variant="default"
|
||||
btnClasses="dark:opacity-50 opacity-60 hover:opacity-100"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
aiChatManager.flowAiChatHelpers?.rejectAllModuleActions()
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -225,7 +225,7 @@ class AIChatManager {
|
||||
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
|
||||
const context = this.contextManager.getSelectedContext()
|
||||
const lang = this.scriptEditorOptions?.lang ?? 'bun'
|
||||
this.tools = [this.changeModeTool, ...prepareScriptTools(currentModel, lang, context)]
|
||||
this.tools = [...prepareScriptTools(currentModel, lang, context)]
|
||||
this.helpers = {
|
||||
getScriptOptions: () => {
|
||||
return {
|
||||
@@ -249,7 +249,7 @@ class AIChatManager {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareFlowSystemMessage(customPrompt)
|
||||
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
|
||||
this.tools = [this.changeModeTool, ...flowTools]
|
||||
this.tools = [...flowTools]
|
||||
this.helpers = this.flowAiChatHelpers
|
||||
} else if (mode === AIMode.NAVIGATOR) {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
@@ -608,9 +608,8 @@ class AIChatManager {
|
||||
|
||||
let snapshot: ExtendedOpenFlow | undefined = undefined
|
||||
if (this.mode === AIMode.FLOW) {
|
||||
this.flowAiChatHelpers!.rejectAllModuleActions()
|
||||
snapshot = this.flowAiChatHelpers!.getFlowAndSelectedId().flow
|
||||
this.flowAiChatHelpers!.setLastSnapshot(snapshot)
|
||||
this.flowAiChatHelpers!.setSnapshot(snapshot)
|
||||
}
|
||||
|
||||
this.displayMessages = [
|
||||
@@ -781,6 +780,18 @@ class AIChatManager {
|
||||
abortController: this.abortController
|
||||
})
|
||||
this.abortController?.abort(cancelReason)
|
||||
// Mark all tool messages in loading state as canceled
|
||||
this.displayMessages = this.displayMessages.map((message) => {
|
||||
if (message.role === 'tool' && message.isLoading) {
|
||||
return {
|
||||
...message,
|
||||
isLoading: false,
|
||||
content: 'Canceled',
|
||||
error: 'Canceled'
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
cancelInlineRequest = (reason?: string) => {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
{:else if message.role === 'tool'}
|
||||
<ToolExecutionDisplay message={message as ToolDisplayMessage} />
|
||||
{:else}
|
||||
{message.content}
|
||||
<span class="whitespace-pre-wrap">{message.content}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
<!-- Expanded Content -->
|
||||
{#if isExpanded}
|
||||
<div class="p-2 bg-surface space-y-3">
|
||||
<!-- Parameters Section - only show if we have parameters -->
|
||||
{#if hasParameters}
|
||||
<!-- Parameters Section - show if we have parameters, or if confirmation is needed (even with empty params) -->
|
||||
{#if hasParameters || message.needsConfirmation}
|
||||
<div class={message.needsConfirmation ? 'opacity-80' : ''}>
|
||||
<ToolContentDisplay
|
||||
title="Parameters"
|
||||
|
||||
@@ -1,78 +1,39 @@
|
||||
<script lang="ts">
|
||||
import FlowModuleSchemaMap from '$lib/components/flows/map/FlowModuleSchemaMap.svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import type { FlowCopilotContext } from '../../flow'
|
||||
import type { ExtendedOpenFlow, FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { dfs } from '$lib/components/flows/previousResults'
|
||||
import { dfs as dfsApply } from '$lib/components/flows/dfs'
|
||||
import { getSubModules } from '$lib/components/flows/flowExplorer'
|
||||
import type { FlowModule, OpenFlow } from '$lib/gen'
|
||||
import { getIndexInNestedModules, getNestedModules } from './utils'
|
||||
import type { AIModuleAction, FlowAIChatHelpers } from './core'
|
||||
import {
|
||||
insertNewFailureModule,
|
||||
insertNewPreprocessorModule
|
||||
} from '$lib/components/flows/flowStateUtils.svelte'
|
||||
import type { InputTransform, OpenFlow } from '$lib/gen'
|
||||
import type { FlowAIChatHelpers } from './core'
|
||||
import { restoreInlineScriptReferences } from './inlineScriptsUtils'
|
||||
import { loadSchemaFromModule } from '$lib/components/flows/flowInfers'
|
||||
import { aiChatManager } from '../AIChatManager.svelte'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import type { AgentTool } from '$lib/components/flows/agentToolUtils'
|
||||
import { getSubModules } from '$lib/components/flows/flowExplorer'
|
||||
import { SPECIAL_MODULE_IDS } from '../shared'
|
||||
import type { FlowCopilotContext } from '../../flow'
|
||||
|
||||
let {
|
||||
flowModuleSchemaMap
|
||||
flowModuleSchemaMap,
|
||||
onTestFlow
|
||||
}: {
|
||||
flowModuleSchemaMap: FlowModuleSchemaMap | undefined
|
||||
onTestFlow?: (conversationId?: string) => Promise<string | undefined>
|
||||
} = $props()
|
||||
|
||||
const { flowStore, flowStateStore, selectionManager, currentEditor } =
|
||||
const { flowStore, flowStateStore, selectionManager, currentEditor, previewArgs } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const selectedId = $derived(selectionManager.getSelectedId())
|
||||
|
||||
const { exprsToSet } = getContext<FlowCopilotContext | undefined>('FlowCopilotContext') ?? {}
|
||||
|
||||
let affectedModules: Record<
|
||||
string,
|
||||
{
|
||||
action: AIModuleAction
|
||||
}
|
||||
> = $state({})
|
||||
let lastSnapshot: ExtendedOpenFlow | undefined = $state(undefined)
|
||||
let previewFlow = $derived.by(() => {
|
||||
const flow = $state.snapshot(flowStore).val
|
||||
if (Object.values(affectedModules).some((m) => m.action === 'removed')) {
|
||||
dfsApply(flow.value.modules, (m, modules) => {
|
||||
const action = affectedModules[m.id]?.action
|
||||
if (action === 'removed') {
|
||||
modules.splice(modules.indexOf(m), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
return flow
|
||||
})
|
||||
|
||||
function setModuleStatus(id: string, action: AIModuleAction) {
|
||||
const existingAction: AIModuleAction | undefined = affectedModules[id]?.action
|
||||
|
||||
if (existingAction === 'added' && action === 'modified') {
|
||||
// means it was added but then edited => keep the action as added
|
||||
action = 'added'
|
||||
} else if (existingAction === 'added' && action === 'removed') {
|
||||
delete affectedModules[id]
|
||||
deleteStep(id)
|
||||
return
|
||||
} else if (existingAction === 'removed' && action === 'added') {
|
||||
action = 'modified'
|
||||
}
|
||||
affectedModules[id] = {
|
||||
action
|
||||
}
|
||||
}
|
||||
// Get diffManager from the graph
|
||||
const diffManager = $derived(flowModuleSchemaMap?.getDiffManager())
|
||||
|
||||
function getModule(id: string, flow: OpenFlow = flowStore.val) {
|
||||
if (id === 'preprocessor') {
|
||||
if (id === SPECIAL_MODULE_IDS.PREPROCESSOR) {
|
||||
return flow.value.preprocessor_module
|
||||
} else if (id === 'failure') {
|
||||
} else if (id === SPECIAL_MODULE_IDS.FAILURE) {
|
||||
return flow.value.failure_module
|
||||
} else {
|
||||
return dfs(id, flow, false)[0]
|
||||
@@ -88,353 +49,6 @@
|
||||
selectedId: selectedId
|
||||
}
|
||||
},
|
||||
// flow apply/reject
|
||||
getPreviewFlow: () => {
|
||||
return $state.snapshot(previewFlow)
|
||||
},
|
||||
hasDiff: () => {
|
||||
return Object.keys(affectedModules).length > 0
|
||||
},
|
||||
acceptAllModuleActions() {
|
||||
for (const id of Object.keys(affectedModules)) {
|
||||
this.acceptModuleAction(id)
|
||||
}
|
||||
},
|
||||
rejectAllModuleActions() {
|
||||
// Do it in reverse to revert nested modules first then parents
|
||||
const ids = Object.keys(affectedModules)
|
||||
for (let i = ids.length - 1; i >= 0; i--) {
|
||||
this.revertModuleAction(ids[i])
|
||||
}
|
||||
affectedModules = {}
|
||||
},
|
||||
setLastSnapshot: (snapshot) => {
|
||||
lastSnapshot = snapshot
|
||||
},
|
||||
revertToSnapshot: (snapshot?: ExtendedOpenFlow) => {
|
||||
affectedModules = {}
|
||||
if (snapshot) {
|
||||
flowStore.val = snapshot
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
if ($currentEditor) {
|
||||
const module = getModule($currentEditor.stepId, snapshot)
|
||||
if (module) {
|
||||
if ($currentEditor.type === 'script' && module.value.type === 'rawscript') {
|
||||
$currentEditor.editor.setCode(module.value.content)
|
||||
} else if ($currentEditor.type === 'iterator' && module.value.type === 'forloopflow') {
|
||||
$currentEditor.editor.setCode(
|
||||
module.value.iterator.type === 'javascript' ? module.value.iterator.expr : ''
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showModuleDiff(id: string) {
|
||||
if (!lastSnapshot) {
|
||||
return
|
||||
}
|
||||
const moduleLastSnapshot = id === 'Input' ? lastSnapshot.schema : getModule(id, lastSnapshot)
|
||||
const currentModule = id === 'Input' ? flowStore.val.schema : getModule(id)
|
||||
|
||||
if (moduleLastSnapshot && currentModule) {
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'simple',
|
||||
title: `Diff for ${id}`,
|
||||
original: moduleLastSnapshot,
|
||||
current: currentModule,
|
||||
button: {
|
||||
text: 'Accept',
|
||||
onClick: () => {
|
||||
diffDrawer?.closeDrawer()
|
||||
this.acceptModuleAction(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
getModuleAction: (id: string) => {
|
||||
return affectedModules[id]?.action
|
||||
},
|
||||
revertModuleAction: (id: string) => {
|
||||
{
|
||||
const action = affectedModules[id]?.action
|
||||
if (action && lastSnapshot) {
|
||||
if (id === 'Input') {
|
||||
flowStore.val.schema = lastSnapshot.schema
|
||||
} else if (action === 'added') {
|
||||
deleteStep(id)
|
||||
} else if (action === 'modified') {
|
||||
const oldModule = getModule(id, lastSnapshot)
|
||||
if (!oldModule) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
const newModule = getModule(id)
|
||||
if (!newModule) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
|
||||
// Apply the old code to the editor and hide diff editor if the reverted module is a rawscript
|
||||
if (
|
||||
newModule.value.type === 'rawscript' &&
|
||||
$currentEditor?.type === 'script' &&
|
||||
$currentEditor.stepId === id
|
||||
) {
|
||||
const aiChatEditorHandler = $currentEditor.editor.getAiChatEditorHandler()
|
||||
if (aiChatEditorHandler) {
|
||||
aiChatEditorHandler.revertAll({ disableReviewCallback: true })
|
||||
$currentEditor.hideDiffMode()
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(newModule).forEach((k) => delete newModule[k])
|
||||
Object.assign(newModule, $state.snapshot(oldModule))
|
||||
}
|
||||
|
||||
refreshStateStore(flowStore)
|
||||
delete affectedModules[id]
|
||||
}
|
||||
}
|
||||
},
|
||||
acceptModuleAction: (id: string) => {
|
||||
if (affectedModules[id]?.action === 'removed') {
|
||||
deleteStep(id)
|
||||
}
|
||||
|
||||
if (
|
||||
affectedModules[id]?.action === 'modified' &&
|
||||
$currentEditor &&
|
||||
$currentEditor.type === 'script' &&
|
||||
$currentEditor.stepId === id
|
||||
) {
|
||||
const aiChatEditorHandler = $currentEditor.editor.getAiChatEditorHandler()
|
||||
if (aiChatEditorHandler) {
|
||||
aiChatEditorHandler.keepAll({ disableReviewCallback: true })
|
||||
}
|
||||
}
|
||||
delete affectedModules[id]
|
||||
},
|
||||
// ai chat tools
|
||||
setCode: async (id, code) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
if (module.value.type === 'rawscript') {
|
||||
module.value.content = code
|
||||
const { input_transforms, schema } = await loadSchemaFromModule(module)
|
||||
module.value.input_transforms = input_transforms
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
if (flowStateStore.val[id]) {
|
||||
flowStateStore.val[id].schema = schema
|
||||
} else {
|
||||
flowStateStore.val[id] = {
|
||||
schema
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Module is not a rawscript or script')
|
||||
}
|
||||
if ($currentEditor && $currentEditor.type === 'script' && $currentEditor.stepId === id) {
|
||||
$currentEditor.editor.setCode(code)
|
||||
}
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
insertStep: async (location, step) => {
|
||||
const { index, modules } =
|
||||
location.type === 'start'
|
||||
? {
|
||||
index: -1,
|
||||
modules: flowStore.val.value.modules
|
||||
}
|
||||
: location.type === 'start_inside_forloop'
|
||||
? {
|
||||
index: -1,
|
||||
modules: getNestedModules(flowStore.val, location.inside)
|
||||
}
|
||||
: location.type === 'start_inside_branch'
|
||||
? {
|
||||
index: -1,
|
||||
modules: getNestedModules(flowStore.val, location.inside, location.branchIndex)
|
||||
}
|
||||
: location.type === 'after'
|
||||
? getIndexInNestedModules(flowStore.val, location.afterId)
|
||||
: {
|
||||
index: -1,
|
||||
modules: flowStore.val.value.modules
|
||||
}
|
||||
|
||||
const indexToInsertAt = index + 1
|
||||
|
||||
let newModules: FlowModule[] | AgentTool[] | undefined = undefined
|
||||
switch (step.type) {
|
||||
case 'rawscript': {
|
||||
const inlineScript = {
|
||||
language: step.language,
|
||||
kind: 'script' as const,
|
||||
subkind: 'flow' as const,
|
||||
summary: step.summary
|
||||
}
|
||||
if (location.type === 'preprocessor') {
|
||||
await insertNewPreprocessorModule(flowStore, flowStateStore, inlineScript)
|
||||
} else if (location.type === 'failure') {
|
||||
await insertNewFailureModule(flowStore, flowStateStore, inlineScript)
|
||||
} else {
|
||||
newModules = await flowModuleSchemaMap?.insertNewModuleAtIndex(
|
||||
modules,
|
||||
indexToInsertAt,
|
||||
'script',
|
||||
undefined,
|
||||
undefined,
|
||||
inlineScript
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'script': {
|
||||
const wsScript = {
|
||||
path: step.path,
|
||||
summary: '',
|
||||
hash: undefined
|
||||
}
|
||||
if (location.type === 'preprocessor') {
|
||||
await insertNewPreprocessorModule(flowStore, flowStateStore, undefined, wsScript)
|
||||
} else if (location.type === 'failure') {
|
||||
await insertNewFailureModule(flowStore, flowStateStore, undefined, wsScript)
|
||||
} else {
|
||||
newModules = await flowModuleSchemaMap?.insertNewModuleAtIndex(
|
||||
modules,
|
||||
indexToInsertAt,
|
||||
'script',
|
||||
wsScript
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'forloop':
|
||||
case 'branchall':
|
||||
case 'branchone': {
|
||||
if (location.type === 'preprocessor' || location.type === 'failure') {
|
||||
throw new Error('Cannot insert a non-script module for preprocessing or error handling')
|
||||
}
|
||||
newModules = await flowModuleSchemaMap?.insertNewModuleAtIndex(
|
||||
modules,
|
||||
indexToInsertAt,
|
||||
step.type
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown step type')
|
||||
}
|
||||
}
|
||||
|
||||
if (location.type === 'preprocessor' || location.type === 'failure') {
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
setModuleStatus(location.type, 'added')
|
||||
|
||||
return location.type
|
||||
} else {
|
||||
const newModule = newModules?.[indexToInsertAt]
|
||||
|
||||
if (!newModule) {
|
||||
throw new Error('Failed to insert module')
|
||||
}
|
||||
|
||||
if (['branchone', 'branchall'].includes(step.type)) {
|
||||
await flowModuleSchemaMap?.addBranch(newModule.id)
|
||||
}
|
||||
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
setModuleStatus(newModule.id, 'added')
|
||||
|
||||
return newModule.id
|
||||
}
|
||||
},
|
||||
removeStep: (id) => {
|
||||
setModuleStatus(id, 'removed')
|
||||
},
|
||||
getStepInputs: async (id) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
const inputs =
|
||||
module.value.type === 'script' || module.value.type === 'rawscript'
|
||||
? module.value.input_transforms
|
||||
: {}
|
||||
|
||||
return inputs
|
||||
},
|
||||
setStepInputs: async (id, inputs) => {
|
||||
if (id === 'preprocessor') {
|
||||
throw new Error('Cannot set inputs for preprocessor')
|
||||
}
|
||||
|
||||
const regex = /\[\[(.+?)\]\]\s*\n([\s\S]*?)(?=\n\[\[|$)/g
|
||||
|
||||
const parsedInputs = Array.from(inputs.matchAll(regex)).map((match) => ({
|
||||
input: match[1],
|
||||
value: match[2].trim()
|
||||
}))
|
||||
|
||||
if (id === selectedId) {
|
||||
exprsToSet?.set({})
|
||||
const argsToUpdate = {}
|
||||
for (const { input, value } of parsedInputs) {
|
||||
argsToUpdate[input] = {
|
||||
type: 'javascript',
|
||||
expr: value
|
||||
}
|
||||
}
|
||||
exprsToSet?.set(argsToUpdate)
|
||||
} else {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
|
||||
if (module.value.type !== 'script' && module.value.type !== 'rawscript') {
|
||||
throw new Error('Module is not a script or rawscript')
|
||||
}
|
||||
|
||||
for (const { input, value } of parsedInputs) {
|
||||
module.value.input_transforms[input] = {
|
||||
type: 'javascript',
|
||||
expr: value
|
||||
}
|
||||
}
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
getFlowInputsSchema: async () => {
|
||||
return flowStore.val.schema ?? {}
|
||||
},
|
||||
setFlowInputsSchema: async (newInputs) => {
|
||||
flowStore.val.schema = newInputs
|
||||
setModuleStatus('Input', 'modified')
|
||||
},
|
||||
selectStep: (id) => {
|
||||
selectionManager.selectId(id)
|
||||
},
|
||||
getStepCode: (id) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
if (module.value.type === 'rawscript') {
|
||||
return module.value.content
|
||||
} else {
|
||||
throw new Error('Module is not a rawscript')
|
||||
}
|
||||
},
|
||||
getModules: (id?: string) => {
|
||||
if (id) {
|
||||
const module = getModule(id)
|
||||
@@ -447,167 +61,215 @@
|
||||
}
|
||||
return flowStore.val.value.modules
|
||||
},
|
||||
setBranchPredicate: async (id, branchIndex, expression) => {
|
||||
setSnapshot: (snapshot: ExtendedOpenFlow) => {
|
||||
diffManager?.setBeforeFlow(snapshot)
|
||||
},
|
||||
revertToSnapshot: (snapshot?: ExtendedOpenFlow) => {
|
||||
if (!diffManager) return
|
||||
|
||||
// Pass snapshot to diffManager - use message's snapshot or fall back to beforeFlow
|
||||
diffManager.revertToSnapshot(flowStore, snapshot)
|
||||
|
||||
// Update current editor if needed
|
||||
const targetSnapshot = snapshot ?? diffManager.beforeFlow
|
||||
if ($currentEditor && targetSnapshot) {
|
||||
const module = getModule($currentEditor.stepId, targetSnapshot)
|
||||
if (module) {
|
||||
if ($currentEditor.type === 'script' && module.value.type === 'rawscript') {
|
||||
$currentEditor.editor.setCode(module.value.content)
|
||||
} else if ($currentEditor.type === 'iterator' && module.value.type === 'forloopflow') {
|
||||
$currentEditor.editor.setCode(
|
||||
module.value.iterator.type === 'javascript' ? module.value.iterator.expr : ''
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ai chat tools
|
||||
setCode: async (id: string, code: string) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
if (module.value.type !== 'branchone') {
|
||||
throw new Error('Module is not a branchall or branchone')
|
||||
}
|
||||
const branch = module.value.branches[branchIndex]
|
||||
if (!branch) {
|
||||
throw new Error('Branch not found')
|
||||
}
|
||||
branch.expr = expression
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
addBranch: async (id) => {
|
||||
flowModuleSchemaMap?.addBranch(id)
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
removeBranch: async (id, branchIndex) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
if (module.value.type !== 'branchall' && module.value.type !== 'branchone') {
|
||||
throw new Error('Module is not a branchall or branchone')
|
||||
}
|
||||
|
||||
// for branch one, we set index + 1 because the removeBranch function assumes the index is shifted by 1 because of the default branch
|
||||
flowModuleSchemaMap?.removeBranch(
|
||||
module.id,
|
||||
module.value.type === 'branchone' ? branchIndex + 1 : branchIndex
|
||||
)
|
||||
refreshStateStore(flowStore)
|
||||
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
setForLoopIteratorExpression: async (id, expression) => {
|
||||
if ($currentEditor && $currentEditor.type === 'iterator' && $currentEditor.stepId === id) {
|
||||
$currentEditor.editor.setCode(expression)
|
||||
} else {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
if (module.value.type === 'rawscript') {
|
||||
// 1. Take snapshot only if none exists (preserves baseline for cumulative changes)
|
||||
if (!diffManager?.beforeFlow) {
|
||||
const snapshot = $state.snapshot(flowStore).val
|
||||
diffManager?.setBeforeFlow(snapshot)
|
||||
diffManager?.setEditMode(true)
|
||||
}
|
||||
if (module.value.type !== 'forloopflow') {
|
||||
throw new Error('Module is not a forloopflow')
|
||||
}
|
||||
module.value.iterator = { type: 'javascript', expr: expression }
|
||||
|
||||
// 2. Apply the code change
|
||||
module.value.content = code
|
||||
const { input_transforms, schema } = await loadSchemaFromModule(module)
|
||||
module.value.input_transforms = input_transforms
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
setForLoopOptions: async (id, opts) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
if (module.value.type !== 'forloopflow') {
|
||||
throw new Error('Module is not a forloopflow')
|
||||
}
|
||||
|
||||
// Apply skip_failures if provided
|
||||
if (typeof opts.skip_failures === 'boolean') {
|
||||
module.value.skip_failures = opts.skip_failures
|
||||
}
|
||||
|
||||
// Apply parallel if provided
|
||||
if (typeof opts.parallel === 'boolean') {
|
||||
module.value.parallel = opts.parallel
|
||||
}
|
||||
|
||||
// Handle parallelism
|
||||
if (opts.parallel === false) {
|
||||
// If parallel is disabled, clear parallelism
|
||||
module.value.parallelism = undefined
|
||||
} else if (opts.parallelism !== undefined) {
|
||||
if (opts.parallelism === null) {
|
||||
// Explicitly clear parallelism
|
||||
module.value.parallelism = undefined
|
||||
} else if (module.value.parallel || opts.parallel === true) {
|
||||
// Only set parallelism if parallel is enabled
|
||||
const n = Math.max(1, Math.floor(Math.abs(opts.parallelism)))
|
||||
module.value.parallelism = {
|
||||
type: 'static',
|
||||
value: n
|
||||
}
|
||||
// Update exprsToSet if this module is currently selected
|
||||
if (id === selectedId && exprsToSet) {
|
||||
exprsToSet.set(input_transforms)
|
||||
}
|
||||
}
|
||||
|
||||
refreshStateStore(flowStore)
|
||||
setModuleStatus(id, 'modified')
|
||||
},
|
||||
setModuleControlOptions: async (id, opts) => {
|
||||
const module = getModule(id)
|
||||
if (!module) {
|
||||
throw new Error('Module not found')
|
||||
}
|
||||
|
||||
// Handle stop_after_if
|
||||
if (typeof opts.stop_after_if === 'boolean') {
|
||||
if (opts.stop_after_if === false) {
|
||||
module.stop_after_if = undefined
|
||||
if (flowStateStore.val[id]) {
|
||||
flowStateStore.val[id].schema = schema
|
||||
} else {
|
||||
module.stop_after_if = {
|
||||
expr: opts.stop_after_if_expr ?? '',
|
||||
skip_if_stopped: opts.stop_after_if
|
||||
flowStateStore.val[id] = {
|
||||
schema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle skip_if
|
||||
if (typeof opts.skip_if === 'boolean') {
|
||||
if (opts.skip_if === false) {
|
||||
module.skip_if = undefined
|
||||
} else {
|
||||
module.skip_if = {
|
||||
expr: opts.skip_if_expr ?? ''
|
||||
// 3. Manually add to moduleActions, preserving existing action types
|
||||
// Note: currentFlow is auto-synced by FlowGraphV2's effect after refreshStateStore
|
||||
const currentAction = diffManager?.moduleActions[id]
|
||||
if (!currentAction) {
|
||||
diffManager?.setModuleActions({
|
||||
...diffManager?.moduleActions,
|
||||
[id]: { action: 'modified', pending: true }
|
||||
})
|
||||
}
|
||||
// If already tracked (e.g., 'added' from setFlowJson), keep that status
|
||||
} else {
|
||||
throw new Error('Module is not a rawscript or script')
|
||||
}
|
||||
if ($currentEditor && $currentEditor.type === 'script' && $currentEditor.stepId === id) {
|
||||
$currentEditor.editor.setCode(code)
|
||||
}
|
||||
},
|
||||
getFlowInputsSchema: async () => {
|
||||
return flowStore.val.schema ?? {}
|
||||
},
|
||||
|
||||
updateExprsToSet: (id: string, inputTransforms: Record<string, InputTransform>) => {
|
||||
if (id === selectedId && exprsToSet) {
|
||||
exprsToSet.set(inputTransforms)
|
||||
}
|
||||
},
|
||||
|
||||
// accept/reject operations (via flowDiffManager)
|
||||
acceptAllModuleActions: () => {
|
||||
diffManager?.acceptAll(flowStore)
|
||||
},
|
||||
rejectAllModuleActions: () => {
|
||||
diffManager?.rejectAll(flowStore)
|
||||
},
|
||||
hasPendingChanges: () => {
|
||||
return diffManager?.hasPendingChanges ?? false
|
||||
},
|
||||
|
||||
selectStep: (id) => {
|
||||
selectionManager.selectId(id)
|
||||
},
|
||||
|
||||
testFlow: async (args, conversationId) => {
|
||||
// Set preview args if provided
|
||||
if (args) {
|
||||
previewArgs.val = args
|
||||
}
|
||||
// Call the UI test function which opens preview panel
|
||||
return await onTestFlow?.(conversationId)
|
||||
},
|
||||
|
||||
setFlowJson: async (json: string) => {
|
||||
try {
|
||||
// Parse JSON to JavaScript object
|
||||
const parsed = JSON.parse(json)
|
||||
|
||||
// Validate that it has the expected structure
|
||||
if (!parsed.modules || !Array.isArray(parsed.modules)) {
|
||||
throw new Error('JSON must contain a "modules" array')
|
||||
}
|
||||
|
||||
// Restore inline script references back to full content
|
||||
const restoredModules = restoreInlineScriptReferences(parsed.modules)
|
||||
|
||||
// Also restore preprocessor and failure modules if they have references
|
||||
let restoredPreprocessor = parsed.preprocessor_module
|
||||
if (
|
||||
restoredPreprocessor?.value?.type === 'rawscript' &&
|
||||
restoredPreprocessor.value.content
|
||||
) {
|
||||
const match = restoredPreprocessor.value.content.match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
// Wrap in array to reuse the restoration function
|
||||
const restored = restoreInlineScriptReferences([restoredPreprocessor])
|
||||
restoredPreprocessor = restored[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshStateStore(flowStore)
|
||||
setModuleStatus(id, 'modified')
|
||||
let restoredFailure = parsed.failure_module
|
||||
if (restoredFailure?.value?.type === 'rawscript' && restoredFailure.value.content) {
|
||||
const match = restoredFailure.value.content.match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
const restored = restoreInlineScriptReferences([restoredFailure])
|
||||
restoredFailure = restored[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Take snapshot of current flowStore BEFORE making changes
|
||||
if (!diffManager?.hasPendingChanges) {
|
||||
const snapshot = $state.snapshot(flowStore).val
|
||||
diffManager?.setBeforeFlow(snapshot)
|
||||
}
|
||||
|
||||
// Directly modify flowStore (immediate effect)
|
||||
flowStore.val.value.modules = restoredModules
|
||||
|
||||
if (parsed.preprocessor_module !== undefined) {
|
||||
flowStore.val.value.preprocessor_module = restoredPreprocessor || undefined
|
||||
}
|
||||
|
||||
if (parsed.failure_module !== undefined) {
|
||||
flowStore.val.value.failure_module = restoredFailure || undefined
|
||||
}
|
||||
|
||||
// Update schema if provided
|
||||
if (parsed.schema !== undefined) {
|
||||
flowStore.val.schema = parsed.schema
|
||||
}
|
||||
|
||||
diffManager?.setEditMode(true)
|
||||
|
||||
// Refresh the state store to update UI
|
||||
// The $effect in FlowGraphV2 will automatically sync currentFlow and currentInputSchema
|
||||
refreshStateStore(flowStore)
|
||||
} catch (error) {
|
||||
console.error('setFlowJson error:', error)
|
||||
throw new Error(
|
||||
`Failed to parse or apply JSON: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteStep(id: string) {
|
||||
flowModuleSchemaMap?.selectNextId(id)
|
||||
if (id === 'preprocessor') {
|
||||
flowStore.val.value.preprocessor_module = undefined
|
||||
} else if (id === 'failure') {
|
||||
flowStore.val.value.failure_module = undefined
|
||||
} else {
|
||||
const { modules } = getIndexInNestedModules(flowStore.val, id)
|
||||
flowModuleSchemaMap?.removeAtId(modules, id)
|
||||
}
|
||||
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
|
||||
const allModuleIds = $derived(dfsApply(flowStore.val.value.modules, (m) => m.id))
|
||||
|
||||
$effect(() => {
|
||||
// remove any affected modules that are no longer in the flow
|
||||
const untrackedAffectedModules = untrack(() => affectedModules)
|
||||
for (const id of Object.keys(untrackedAffectedModules)) {
|
||||
if (!allModuleIds.includes(id)) {
|
||||
delete affectedModules[id]
|
||||
if (
|
||||
$currentEditor?.type === 'script' &&
|
||||
selectedId &&
|
||||
diffManager?.moduleActions[selectedId]?.pending &&
|
||||
$currentEditor.editor.getAiChatEditorHandler()
|
||||
) {
|
||||
const moduleLastSnapshot = getModule(selectedId, diffManager.beforeFlow)
|
||||
const content =
|
||||
moduleLastSnapshot?.value.type === 'rawscript' ? moduleLastSnapshot.value.content : ''
|
||||
if (content.length > 0) {
|
||||
untrack(() =>
|
||||
$currentEditor.editor.reviewAppliedCode(content, {
|
||||
onFinishedReview: () => {
|
||||
diffManager?.acceptModule(selectedId, flowStore)
|
||||
$currentEditor.hideDiffMode()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const cleanup = aiChatManager.setFlowHelpers(flowHelpers)
|
||||
return cleanup
|
||||
return () => {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
@@ -617,39 +279,15 @@
|
||||
flowStateStore.val,
|
||||
$currentEditor
|
||||
)
|
||||
return cleanup
|
||||
return () => {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const cleanup = aiChatManager.listenForCurrentEditorChanges($currentEditor)
|
||||
return cleanup
|
||||
})
|
||||
|
||||
// Automatically show revert review when selecting a rawscript module with pending changes
|
||||
$effect(() => {
|
||||
if (
|
||||
$currentEditor?.type === 'script' &&
|
||||
selectedId &&
|
||||
affectedModules[selectedId] &&
|
||||
$currentEditor.editor.getAiChatEditorHandler()
|
||||
) {
|
||||
const moduleLastSnapshot = getModule(selectedId, lastSnapshot)
|
||||
const content =
|
||||
moduleLastSnapshot?.value.type === 'rawscript' ? moduleLastSnapshot.value.content : ''
|
||||
if (content.length > 0) {
|
||||
untrack(() =>
|
||||
$currentEditor.editor.reviewAppliedCode(content, {
|
||||
onFinishedReview: () => {
|
||||
flowHelpers.acceptModuleAction(selectedId)
|
||||
$currentEditor.hideDiffMode()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
let diffDrawer: DiffDrawer | undefined = $state(undefined)
|
||||
</script>
|
||||
|
||||
<DiffDrawer bind:this={diffDrawer} />
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<script module lang="ts">
|
||||
export const getAiModuleAction = (id: string | undefined) => {
|
||||
if (!id) return undefined
|
||||
return aiChatManager.flowAiChatHelpers?.getModuleAction(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Check, DiffIcon, X } from 'lucide-svelte'
|
||||
import { aiChatManager } from '../AIChatManager.svelte'
|
||||
import type { AIModuleAction } from './core'
|
||||
|
||||
let {
|
||||
id,
|
||||
action,
|
||||
placement = 'top'
|
||||
}: {
|
||||
id: string | undefined
|
||||
action: AIModuleAction | undefined
|
||||
placement?: 'top' | 'bottom'
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
{#if action && id}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute right-0 left-0 flex flex-row ',
|
||||
placement === 'top' ? 'top-0 -translate-y-full' : 'bottom-0 translate-y-full',
|
||||
action === 'modified' ? 'justify-between' : 'justify-end'
|
||||
)}
|
||||
>
|
||||
{#if action === 'modified'}
|
||||
<button
|
||||
class="p-1 bg-surface hover:bg-surface-hover rounded-t-md text-3xs font-normal flex flex-row items-center gap-1 text-orange-800 dark:text-orange-400"
|
||||
onclick={() => {
|
||||
aiChatManager.flowAiChatHelpers?.showModuleDiff(id)
|
||||
}}
|
||||
>
|
||||
<DiffIcon size={14} /> Diff
|
||||
</button>
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-row bg-surface overflow-hidden',
|
||||
placement === 'top' ? 'rounded-t-md' : 'rounded-b-md'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
class="p-1 bg-green-500 text-white hover:bg-green-600 text-3xs font-normal flex flex-row items-center gap-1"
|
||||
onclick={() => aiChatManager.flowAiChatHelpers?.acceptModuleAction(id)}
|
||||
>
|
||||
<Check size={14} /> Accept
|
||||
</button>
|
||||
<button
|
||||
class="p-1 hover:bg-red-500 hover:text-white text-3xs font-normal flex flex-row items-center gap-1"
|
||||
onclick={() => aiChatManager.flowAiChatHelpers?.revertModuleAction(id)}
|
||||
>
|
||||
<X size={14} /> Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
|
||||
/**
|
||||
* Storage for inline scripts extracted from flow modules.
|
||||
* Maps module IDs to their rawscript content for token-efficient transmission to AI.
|
||||
*/
|
||||
class InlineScriptStore {
|
||||
private scripts: Map<string, string> = new Map()
|
||||
|
||||
clear() {
|
||||
this.scripts.clear()
|
||||
}
|
||||
|
||||
set(moduleId: string, content: string) {
|
||||
this.scripts.set(moduleId, content)
|
||||
}
|
||||
|
||||
get(moduleId: string): string | undefined {
|
||||
return this.scripts.get(moduleId)
|
||||
}
|
||||
|
||||
has(moduleId: string): boolean {
|
||||
return this.scripts.has(moduleId)
|
||||
}
|
||||
|
||||
getAll(): Record<string, string> {
|
||||
return Object.fromEntries(this.scripts.entries())
|
||||
}
|
||||
}
|
||||
|
||||
export const inlineScriptStore = new InlineScriptStore()
|
||||
|
||||
/**
|
||||
* Recursively extracts all rawscript content from flow modules and stores them.
|
||||
* Replaces the content with references like "inline_script.{module_id}".
|
||||
*/
|
||||
export function extractAndReplaceInlineScripts(modules: FlowModule[]): FlowModule[] {
|
||||
if (!modules || !Array.isArray(modules)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return modules.map((module) => {
|
||||
const newModule = { ...module }
|
||||
|
||||
if (newModule.value.type === 'rawscript' && newModule.value.content) {
|
||||
// Store the original content
|
||||
inlineScriptStore.set(module.id, newModule.value.content)
|
||||
|
||||
// Replace with reference
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
content: `inline_script.${module.id}`
|
||||
}
|
||||
} else if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') {
|
||||
// Recursively process nested modules in loops
|
||||
if (newModule.value.modules) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
modules: extractAndReplaceInlineScripts(newModule.value.modules)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchone') {
|
||||
// Process branches and default modules
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? extractAndReplaceInlineScripts(branch.modules) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModule.value.default) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
default: extractAndReplaceInlineScripts(newModule.value.default)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchall') {
|
||||
// Process all branches
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? extractAndReplaceInlineScripts(branch.modules) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'aiagent') {
|
||||
// Process AI agent tools
|
||||
if (newModule.value.tools) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
tools: newModule.value.tools.map((tool) => {
|
||||
if (
|
||||
tool.value &&
|
||||
'tool_type' in tool.value &&
|
||||
tool.value.tool_type === 'flowmodule' &&
|
||||
'type' in tool.value &&
|
||||
tool.value.type === 'rawscript' &&
|
||||
'content' in tool.value &&
|
||||
tool.value.content
|
||||
) {
|
||||
inlineScriptStore.set(tool.id, tool.value.content as string)
|
||||
return {
|
||||
...tool,
|
||||
value: {
|
||||
...tool.value,
|
||||
content: `inline_script.${tool.id}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return tool
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newModule
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively restores inline script references back to their full content.
|
||||
* If content matches pattern "inline_script.{id}", looks up and restores the original.
|
||||
* If content doesn't match (new/modified script), keeps it as-is.
|
||||
*/
|
||||
export function restoreInlineScriptReferences(modules: FlowModule[]): FlowModule[] {
|
||||
return modules.map((module) => {
|
||||
const newModule = { ...module }
|
||||
|
||||
if (newModule.value.type === 'rawscript' && newModule.value.content) {
|
||||
const content = newModule.value.content
|
||||
// Check if it's a reference
|
||||
const match = content.match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
const moduleId = match[1]
|
||||
const storedContent = inlineScriptStore.get(moduleId)
|
||||
if (storedContent !== undefined) {
|
||||
// Restore original content
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
content: storedContent
|
||||
}
|
||||
}
|
||||
// If not found in store, keep the reference as-is (shouldn't happen normally)
|
||||
}
|
||||
// If not a reference, it's new/modified content - keep as-is
|
||||
} else if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') {
|
||||
// Recursively process nested modules in loops
|
||||
if (newModule.value.modules) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
modules: restoreInlineScriptReferences(newModule.value.modules)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchone') {
|
||||
// Process branches and default modules
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? restoreInlineScriptReferences(branch.modules) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModule.value.default) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
default: restoreInlineScriptReferences(newModule.value.default)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchall') {
|
||||
// Process all branches
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? restoreInlineScriptReferences(branch.modules) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'aiagent') {
|
||||
// Process AI agent tools
|
||||
if (newModule.value.tools) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
tools: newModule.value.tools.map((tool) => {
|
||||
if (
|
||||
tool.value &&
|
||||
'tool_type' in tool.value &&
|
||||
tool.value.tool_type === 'flowmodule' &&
|
||||
'type' in tool.value &&
|
||||
tool.value.type === 'rawscript' &&
|
||||
'content' in tool.value &&
|
||||
tool.value.content
|
||||
) {
|
||||
const content = tool.value.content as string
|
||||
const match = content.match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
const toolId = match[1]
|
||||
const storedContent = inlineScriptStore.get(toolId)
|
||||
if (storedContent !== undefined) {
|
||||
return {
|
||||
...tool,
|
||||
value: {
|
||||
...tool.value,
|
||||
content: storedContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tool
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newModule
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively finds any unresolved inline script references in flow modules.
|
||||
* Returns array of module IDs that still have `inline_script.{id}` patterns.
|
||||
*/
|
||||
export function findUnresolvedInlineScriptRefs(modules: FlowModule[]): string[] {
|
||||
const unresolvedRefs: string[] = []
|
||||
|
||||
function checkModule(module: FlowModule) {
|
||||
if (module.value.type === 'rawscript' && module.value.content) {
|
||||
const match = module.value.content.match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
unresolvedRefs.push(match[1])
|
||||
}
|
||||
} else if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
|
||||
if (module.value.modules) {
|
||||
module.value.modules.forEach(checkModule)
|
||||
}
|
||||
} else if (module.value.type === 'branchone') {
|
||||
if (module.value.branches) {
|
||||
module.value.branches.forEach((branch) => {
|
||||
branch.modules?.forEach(checkModule)
|
||||
})
|
||||
}
|
||||
if (module.value.default) {
|
||||
module.value.default.forEach(checkModule)
|
||||
}
|
||||
} else if (module.value.type === 'branchall') {
|
||||
if (module.value.branches) {
|
||||
module.value.branches.forEach((branch) => {
|
||||
branch.modules?.forEach(checkModule)
|
||||
})
|
||||
}
|
||||
} else if (module.value.type === 'aiagent') {
|
||||
// Check AI agent tools
|
||||
if (module.value.tools) {
|
||||
for (const tool of module.value.tools) {
|
||||
if (
|
||||
tool.value &&
|
||||
'tool_type' in tool.value &&
|
||||
tool.value.tool_type === 'flowmodule' &&
|
||||
'type' in tool.value &&
|
||||
tool.value.type === 'rawscript' &&
|
||||
'content' in tool.value &&
|
||||
tool.value.content
|
||||
) {
|
||||
const match = (tool.value.content as string).match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
unresolvedRefs.push(match[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules.forEach(checkModule)
|
||||
return unresolvedRefs
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,17 +1,31 @@
|
||||
import { dfs } from '$lib/components/flows/previousResults'
|
||||
import type { OpenFlow } from '$lib/gen'
|
||||
import type { AIModuleAction } from './core'
|
||||
import type { FlowModule, OpenFlow } from '$lib/gen'
|
||||
|
||||
export function getIndexInNestedModules(flow: OpenFlow, id: string) {
|
||||
// Helper to find module by ID in a flow
|
||||
export function getModuleById(flow: OpenFlow, moduleId: string): FlowModule | undefined {
|
||||
const allModules = dfs(moduleId, flow, false)
|
||||
return allModules[0]
|
||||
}
|
||||
|
||||
export function getIndexInNestedModules(
|
||||
flow: OpenFlow,
|
||||
id: string
|
||||
): { index: number; modules: FlowModule[] } | null {
|
||||
const accessingModules = dfs(id, flow, true).reverse()
|
||||
|
||||
if (accessingModules.length === 0) {
|
||||
// Module not found in flow
|
||||
return null
|
||||
}
|
||||
|
||||
let parent = flow.value.modules
|
||||
let lastIndex = -1
|
||||
for (const [ai, am] of accessingModules.entries()) {
|
||||
const index = parent.findIndex((m) => m.id === am.id)
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Module not found: ${am.id} in ${parent.map((m) => m.id).join(', ')}`)
|
||||
// Module no longer exists in expected location (may have been deleted with parent)
|
||||
return null
|
||||
}
|
||||
|
||||
lastIndex = index
|
||||
@@ -33,18 +47,18 @@ export function getIndexInNestedModules(flow: OpenFlow, id: string) {
|
||||
b.modules.some((m) => m.id === accessingModules[ai + 1].id)
|
||||
)
|
||||
if (branchIdx === -1) {
|
||||
throw new Error(
|
||||
`Branch not found: ${am.id} in ${parent[index].value.branches.map((b) => b.modules.map((m) => m.id).join(', ')).join(';')}`
|
||||
)
|
||||
// Module no longer exists in branch (may have been deleted)
|
||||
return null
|
||||
}
|
||||
parent = parent[index].value.branches[branchIdx].modules
|
||||
} else {
|
||||
throw new Error('Module is not a for loop or branch')
|
||||
// Unexpected module type in path
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (lastIndex === -1) {
|
||||
throw new Error('Module not found, should have been caught earlier')
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -53,7 +67,11 @@ export function getIndexInNestedModules(flow: OpenFlow, id: string) {
|
||||
}
|
||||
}
|
||||
export function getNestedModules(flow: OpenFlow, id: string, branchIndex?: number) {
|
||||
const { index, modules } = getIndexInNestedModules(flow, id)
|
||||
const result = getIndexInNestedModules(flow, id)
|
||||
if (!result) {
|
||||
throw new Error(`Module not found: ${id}`)
|
||||
}
|
||||
const { index, modules } = result
|
||||
|
||||
// we know index is correct because we've already checked it in getIndexInNestedModules
|
||||
const module = modules[index]
|
||||
@@ -84,46 +102,716 @@ export function getNestedModules(flow: OpenFlow, id: string, branchIndex?: numbe
|
||||
}
|
||||
}
|
||||
|
||||
export function aiModuleActionToBgColor(action: AIModuleAction | undefined) {
|
||||
switch (action) {
|
||||
case 'modified':
|
||||
return '!bg-orange-200 dark:!bg-orange-800'
|
||||
case 'added':
|
||||
return '!bg-green-200 dark:!bg-green-800'
|
||||
case 'removed':
|
||||
return '!bg-red-200/50 dark:!bg-red-800/50'
|
||||
case 'shadowed':
|
||||
return '!bg-gray-200/30 dark:!bg-gray-800/30 !opacity-50'
|
||||
default:
|
||||
return ''
|
||||
/**
|
||||
* Recursively resolves all $ref references in a JSON Schema by inlining them.
|
||||
* This ensures the schema is fully self-contained for AI providers that don't
|
||||
* support external references or have strict schema validation (e.g., Google/Gemini).
|
||||
*
|
||||
* @param schema - The schema object to resolve
|
||||
* @param rootSchema - The root schema document containing all definitions
|
||||
* @param visited - Set of visited $ref paths to prevent infinite recursion
|
||||
* @returns Fully resolved schema with all $ref references inlined
|
||||
*/
|
||||
export function resolveSchemaRefs(schema: any, rootSchema: any, visited = new Set<string>()): any {
|
||||
if (!schema || typeof schema !== 'object') return schema
|
||||
|
||||
// Handle $ref
|
||||
if (schema.$ref) {
|
||||
const refPath = schema.$ref.replace('#/', '').split('/')
|
||||
|
||||
// Prevent infinite recursion with circular refs
|
||||
if (visited.has(schema.$ref)) {
|
||||
return { type: 'object' } // Fallback for circular refs
|
||||
}
|
||||
visited.add(schema.$ref)
|
||||
|
||||
let resolved = rootSchema
|
||||
for (const part of refPath) {
|
||||
resolved = resolved[part]
|
||||
}
|
||||
|
||||
// Recursively resolve the referenced schema
|
||||
return resolveSchemaRefs(resolved, rootSchema, new Set(visited))
|
||||
}
|
||||
}
|
||||
export function aiModuleActionToBorderColor(action: AIModuleAction | undefined) {
|
||||
switch (action) {
|
||||
case 'modified':
|
||||
return '!border-orange-300 dark:!border-orange-700'
|
||||
case 'added':
|
||||
return '!border-green-400 dark:!border-green-700'
|
||||
case 'removed':
|
||||
return '!border-red-300 dark:!border-red-700'
|
||||
case 'shadowed':
|
||||
return '!border-gray-300 dark:!border-gray-600'
|
||||
default:
|
||||
return ''
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(schema)) {
|
||||
return schema.map((item) => resolveSchemaRefs(item, rootSchema, visited))
|
||||
}
|
||||
|
||||
// Handle objects - recursively process all properties
|
||||
const result: any = {}
|
||||
for (const key in schema) {
|
||||
result[key] = resolveSchemaRefs(schema[key], rootSchema, visited)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function aiModuleActionToTextColor(action: AIModuleAction | undefined) {
|
||||
switch (action) {
|
||||
case 'modified':
|
||||
return '!text-orange-800 dark:!text-orange-200'
|
||||
case 'added':
|
||||
return '!text-green-800 dark:!text-green-200'
|
||||
case 'removed':
|
||||
return '!text-red-800 dark:!text-red-200'
|
||||
case 'shadowed':
|
||||
return '!text-gray-600 dark:!text-gray-400'
|
||||
default:
|
||||
return ''
|
||||
/**
|
||||
* Recursively collects all module IDs from a module and its nested structures
|
||||
*/
|
||||
export function collectAllModuleIds(module: FlowModule): string[] {
|
||||
const ids: string[] = [module.id]
|
||||
|
||||
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
|
||||
if (module.value.modules) {
|
||||
for (const nested of module.value.modules) {
|
||||
ids.push(...collectAllModuleIds(nested))
|
||||
}
|
||||
}
|
||||
} else if (module.value.type === 'branchone') {
|
||||
if (module.value.branches) {
|
||||
for (const branch of module.value.branches) {
|
||||
if (branch.modules) {
|
||||
for (const nested of branch.modules) {
|
||||
ids.push(...collectAllModuleIds(nested))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (module.value.default) {
|
||||
for (const nested of module.value.default) {
|
||||
ids.push(...collectAllModuleIds(nested))
|
||||
}
|
||||
}
|
||||
} else if (module.value.type === 'branchall') {
|
||||
if (module.value.branches) {
|
||||
for (const branch of module.value.branches) {
|
||||
if (branch.modules) {
|
||||
for (const nested of branch.modules) {
|
||||
ids.push(...collectAllModuleIds(nested))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (module.value.type === 'aiagent') {
|
||||
if (module.value.tools) {
|
||||
for (const tool of module.value.tools) {
|
||||
ids.push(tool.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively finds a module by ID in the flow structure
|
||||
*/
|
||||
export function findModuleInFlow(modules: FlowModule[], id: string): FlowModule | undefined {
|
||||
for (const module of modules) {
|
||||
if (module.id === id) {
|
||||
return module
|
||||
}
|
||||
|
||||
// Search in nested structures
|
||||
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
|
||||
if (module.value.modules) {
|
||||
const found = findModuleInFlow(module.value.modules, id)
|
||||
if (found) return found
|
||||
}
|
||||
} else if (module.value.type === 'branchone') {
|
||||
if (module.value.branches) {
|
||||
for (const branch of module.value.branches) {
|
||||
if (branch.modules) {
|
||||
const found = findModuleInFlow(branch.modules, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
}
|
||||
if (module.value.default) {
|
||||
const found = findModuleInFlow(module.value.default, id)
|
||||
if (found) return found
|
||||
}
|
||||
} else if (module.value.type === 'branchall') {
|
||||
if (module.value.branches) {
|
||||
for (const branch of module.value.branches) {
|
||||
if (branch.modules) {
|
||||
const found = findModuleInFlow(branch.modules, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (module.value.type === 'aiagent') {
|
||||
// Search in AI agent tools
|
||||
if (module.value.tools) {
|
||||
for (const tool of module.value.tools) {
|
||||
if (tool.id === id) {
|
||||
// Return a pseudo-FlowModule for compatibility
|
||||
return { id: tool.id, value: tool.value, summary: tool.summary } as FlowModule
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively removes a module by ID from the flow structure
|
||||
* Returns the updated modules array
|
||||
*/
|
||||
export function removeModuleFromFlow(modules: FlowModule[], id: string): FlowModule[] {
|
||||
const result: FlowModule[] = []
|
||||
|
||||
for (const module of modules) {
|
||||
if (module.id === id) {
|
||||
// Skip this module (remove it)
|
||||
continue
|
||||
}
|
||||
|
||||
const newModule = { ...module }
|
||||
|
||||
// Recursively remove from nested structures
|
||||
if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') {
|
||||
if (newModule.value.modules) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
modules: removeModuleFromFlow(newModule.value.modules, id)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchone') {
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? removeModuleFromFlow(branch.modules, id) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModule.value.default) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
default: removeModuleFromFlow(newModule.value.default, id)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchall') {
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? removeModuleFromFlow(branch.modules, id) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'aiagent') {
|
||||
// Remove tool from AI agent's tools array
|
||||
if (newModule.value.tools) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
tools: newModule.value.tools.filter((tool) => tool.id !== id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(newModule)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively removes a branch by index from a branchall/branchone container
|
||||
* Returns the updated modules array
|
||||
*/
|
||||
export function removeBranchFromFlow(
|
||||
modules: FlowModule[],
|
||||
containerId: string,
|
||||
branchIndex: number
|
||||
): FlowModule[] {
|
||||
return modules.map((module) => {
|
||||
if (module.id === containerId) {
|
||||
if (module.value.type === 'branchall') {
|
||||
const branches = module.value.branches || []
|
||||
if (branchIndex < 0 || branchIndex >= branches.length) {
|
||||
throw new Error(`Branch index ${branchIndex} out of bounds (0-${branches.length - 1})`)
|
||||
}
|
||||
return {
|
||||
...module,
|
||||
value: {
|
||||
...module.value,
|
||||
branches: branches.filter((_, i) => i !== branchIndex)
|
||||
}
|
||||
} as FlowModule
|
||||
}
|
||||
if (module.value.type === 'branchone') {
|
||||
const branches = module.value.branches || []
|
||||
if (branchIndex < 0 || branchIndex >= branches.length) {
|
||||
throw new Error(`Branch index ${branchIndex} out of bounds (0-${branches.length - 1})`)
|
||||
}
|
||||
return {
|
||||
...module,
|
||||
value: {
|
||||
...module.value,
|
||||
branches: branches.filter((_, i) => i !== branchIndex)
|
||||
}
|
||||
} as FlowModule
|
||||
}
|
||||
throw new Error(`Module '${containerId}' is not a branchall/branchone container`)
|
||||
}
|
||||
|
||||
// Recursively search nested structures
|
||||
const newModule = { ...module }
|
||||
if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') {
|
||||
if (newModule.value.modules) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
modules: removeBranchFromFlow(newModule.value.modules, containerId, branchIndex)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchone') {
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules
|
||||
? removeBranchFromFlow(branch.modules, containerId, branchIndex)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModule.value.default) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
default: removeBranchFromFlow(newModule.value.default, containerId, branchIndex)
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchall') {
|
||||
if (newModule.value.branches) {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules
|
||||
? removeBranchFromFlow(branch.modules, containerId, branchIndex)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newModule
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a branch path string into navigation components
|
||||
* Examples: 'branches.0' -> {type: 'branches', index: 0}
|
||||
* 'default' -> {type: 'default'}
|
||||
* 'modules' -> {type: 'modules'}
|
||||
*/
|
||||
export function parseBranchPath(path: string): { type: string; index?: number } {
|
||||
if (path === 'default') {
|
||||
return { type: 'default' }
|
||||
}
|
||||
if (path === 'modules') {
|
||||
return { type: 'modules' }
|
||||
}
|
||||
if (path === 'tools') {
|
||||
return { type: 'tools' }
|
||||
}
|
||||
|
||||
const match = path.match(/^(branches)\.(\d+)$/)
|
||||
if (match) {
|
||||
return { type: match[1], index: parseInt(match[2], 10) }
|
||||
}
|
||||
|
||||
throw new Error(`Invalid branch path: ${path}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target array for module insertion based on insideId and branchPath
|
||||
*/
|
||||
function getTargetArray(
|
||||
modules: FlowModule[],
|
||||
insideId: string,
|
||||
branchPath: string
|
||||
): FlowModule[] | undefined {
|
||||
const container = findModuleInFlow(modules, insideId)
|
||||
if (!container) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsed = parseBranchPath(branchPath)
|
||||
|
||||
if (container.value.type === 'forloopflow' || container.value.type === 'whileloopflow') {
|
||||
if (parsed.type === 'modules') {
|
||||
return container.value.modules || []
|
||||
}
|
||||
throw new Error(`Invalid branchPath '${branchPath}' for loop module. Use 'modules'`)
|
||||
} else if (container.value.type === 'branchone') {
|
||||
if (parsed.type === 'branches' && parsed.index !== undefined) {
|
||||
return container.value.branches?.[parsed.index]?.modules
|
||||
} else if (parsed.type === 'default') {
|
||||
return container.value.default
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid branchPath '${branchPath}' for branchone module. Use 'branches.N' or 'default'`
|
||||
)
|
||||
} else if (container.value.type === 'branchall') {
|
||||
if (parsed.type === 'branches' && parsed.index !== undefined) {
|
||||
return container.value.branches?.[parsed.index]?.modules
|
||||
}
|
||||
throw new Error(`Invalid branchPath '${branchPath}' for branchall module. Use 'branches.N'`)
|
||||
} else if (container.value.type === 'aiagent') {
|
||||
if (parsed.type === 'tools') {
|
||||
// Return tools array (AgentTool[]), caller handles the different structure
|
||||
return (container.value.tools as any) || []
|
||||
}
|
||||
throw new Error(`Invalid branchPath '${branchPath}' for aiagent module. Use 'tools'`)
|
||||
}
|
||||
|
||||
throw new Error(`Module '${insideId}' is not a container type`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a nested array within a container module
|
||||
*/
|
||||
function updateNestedArray(
|
||||
module: FlowModule,
|
||||
branchPath: string,
|
||||
updatedArray: FlowModule[]
|
||||
): FlowModule {
|
||||
const parsed = parseBranchPath(branchPath)
|
||||
const newModule = { ...module }
|
||||
|
||||
if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') {
|
||||
if (parsed.type === 'modules') {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
modules: updatedArray
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchone') {
|
||||
if (parsed.type === 'branches' && parsed.index !== undefined && newModule.value.branches) {
|
||||
const newBranches = [...newModule.value.branches]
|
||||
newBranches[parsed.index] = {
|
||||
...newBranches[parsed.index],
|
||||
modules: updatedArray
|
||||
}
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newBranches
|
||||
}
|
||||
} else if (parsed.type === 'default') {
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
default: updatedArray
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'branchall') {
|
||||
if (parsed.type === 'branches' && parsed.index !== undefined && newModule.value.branches) {
|
||||
const newBranches = [...newModule.value.branches]
|
||||
newBranches[parsed.index] = {
|
||||
...newBranches[parsed.index],
|
||||
modules: updatedArray
|
||||
}
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
branches: newBranches
|
||||
}
|
||||
}
|
||||
} else if (newModule.value.type === 'aiagent') {
|
||||
if (parsed.type === 'tools') {
|
||||
// Note: updatedArray is actually AgentTool[] when dealing with AI agents
|
||||
newModule.value = {
|
||||
...newModule.value,
|
||||
tools: updatedArray as any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newModule
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adds a module to the flow structure
|
||||
*/
|
||||
export function addModuleToFlow(
|
||||
modules: FlowModule[],
|
||||
afterId: string | null,
|
||||
insideId: string | null,
|
||||
branchPath: string | null,
|
||||
newModule: FlowModule
|
||||
): FlowModule[] {
|
||||
// Case 1a: Adding a NEW branch to branchall/branchone (insideId set, branchPath null)
|
||||
if (insideId && branchPath === null) {
|
||||
return modules.map((module) => {
|
||||
if (module.id === insideId) {
|
||||
// Adding a new branch to branchall
|
||||
if (module.value.type === 'branchall') {
|
||||
const newBranch = {
|
||||
summary: (newModule as any).summary || '',
|
||||
skip_failure: (newModule as any).skip_failure ?? false,
|
||||
modules: (newModule as any).modules || []
|
||||
}
|
||||
return {
|
||||
...module,
|
||||
value: {
|
||||
...module.value,
|
||||
branches: [...(module.value.branches || []), newBranch]
|
||||
}
|
||||
} as FlowModule
|
||||
}
|
||||
// Adding a new branch to branchone
|
||||
if (module.value.type === 'branchone') {
|
||||
const newBranch = {
|
||||
summary: (newModule as any).summary || '',
|
||||
expr: (newModule as any).expr || 'false',
|
||||
modules: (newModule as any).modules || []
|
||||
}
|
||||
return {
|
||||
...module,
|
||||
value: {
|
||||
...module.value,
|
||||
branches: [...(module.value.branches || []), newBranch]
|
||||
}
|
||||
} as FlowModule
|
||||
}
|
||||
throw new Error(
|
||||
`Cannot add branch to module '${insideId}': branchPath=null is only valid for branchall/branchone containers`
|
||||
)
|
||||
}
|
||||
|
||||
// Recursively search nested structures for the target container
|
||||
const newModuleCopy = { ...module }
|
||||
if (
|
||||
newModuleCopy.value.type === 'forloopflow' ||
|
||||
newModuleCopy.value.type === 'whileloopflow'
|
||||
) {
|
||||
if (newModuleCopy.value.modules) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
modules: addModuleToFlow(
|
||||
newModuleCopy.value.modules,
|
||||
afterId,
|
||||
insideId,
|
||||
branchPath,
|
||||
newModule
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'branchone') {
|
||||
if (newModuleCopy.value.branches) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
branches: newModuleCopy.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules
|
||||
? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModuleCopy.value.default) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
default: addModuleToFlow(
|
||||
newModuleCopy.value.default,
|
||||
afterId,
|
||||
insideId,
|
||||
branchPath,
|
||||
newModule
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'branchall') {
|
||||
if (newModuleCopy.value.branches) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
branches: newModuleCopy.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules
|
||||
? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
return newModuleCopy
|
||||
})
|
||||
}
|
||||
|
||||
// Case 1b: Adding inside a container (insideId + branchPath both set)
|
||||
if (insideId && branchPath) {
|
||||
return modules.map((module) => {
|
||||
if (module.id === insideId) {
|
||||
// Special handling for AI agent tools
|
||||
if (module.value.type === 'aiagent' && branchPath === 'tools') {
|
||||
// For AI agents, newModule structure is { id, summary, value: { tool_type, ...FlowModuleValue } }
|
||||
// The value should already include tool_type from the caller
|
||||
const newTool = {
|
||||
id: newModule.id,
|
||||
summary: newModule.summary,
|
||||
value: newModule.value as any
|
||||
}
|
||||
return {
|
||||
...module,
|
||||
value: {
|
||||
...module.value,
|
||||
tools: [...(module.value.tools || []), newTool]
|
||||
}
|
||||
} as FlowModule
|
||||
}
|
||||
|
||||
const targetArray = getTargetArray(modules, insideId, branchPath)
|
||||
if (!targetArray) {
|
||||
throw new Error(
|
||||
`Cannot find target array for insideId '${insideId}' with branchPath '${branchPath}'`
|
||||
)
|
||||
}
|
||||
const updatedArray =
|
||||
afterId !== null
|
||||
? addModuleToFlow(targetArray, afterId, null, null, newModule)
|
||||
: [newModule, ...targetArray] // afterId null = insert at beginning
|
||||
return updateNestedArray(module, branchPath, updatedArray)
|
||||
}
|
||||
|
||||
// Recursively search nested structures
|
||||
const newModuleCopy = { ...module }
|
||||
if (
|
||||
newModuleCopy.value.type === 'forloopflow' ||
|
||||
newModuleCopy.value.type === 'whileloopflow'
|
||||
) {
|
||||
if (newModuleCopy.value.modules) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
modules: addModuleToFlow(
|
||||
newModuleCopy.value.modules,
|
||||
afterId,
|
||||
insideId,
|
||||
branchPath,
|
||||
newModule
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'branchone') {
|
||||
if (newModuleCopy.value.branches) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
branches: newModuleCopy.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules
|
||||
? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModuleCopy.value.default) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
default: addModuleToFlow(
|
||||
newModuleCopy.value.default,
|
||||
afterId,
|
||||
insideId,
|
||||
branchPath,
|
||||
newModule
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'branchall') {
|
||||
if (newModuleCopy.value.branches) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
branches: newModuleCopy.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules
|
||||
? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newModuleCopy
|
||||
})
|
||||
}
|
||||
|
||||
// Case 2: Adding at current level after a specific module
|
||||
if (afterId !== null) {
|
||||
const result: FlowModule[] = []
|
||||
for (const module of modules) {
|
||||
result.push(module)
|
||||
if (module.id === afterId) {
|
||||
result.push(newModule)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Case 3: afterId is null - insert at the beginning
|
||||
return [newModule, ...modules]
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively replaces a module by ID
|
||||
*/
|
||||
export function replaceModuleInFlow(
|
||||
modules: FlowModule[],
|
||||
id: string,
|
||||
newModule: FlowModule
|
||||
): FlowModule[] {
|
||||
return modules.map((module) => {
|
||||
if (module.id === id) {
|
||||
return { ...newModule, id } // Ensure ID remains the same
|
||||
}
|
||||
|
||||
const newModuleCopy = { ...module }
|
||||
|
||||
// Recursively replace in nested structures
|
||||
if (
|
||||
newModuleCopy.value.type === 'forloopflow' ||
|
||||
newModuleCopy.value.type === 'whileloopflow'
|
||||
) {
|
||||
if (newModuleCopy.value.modules) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
modules: replaceModuleInFlow(newModuleCopy.value.modules, id, newModule)
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'branchone') {
|
||||
if (newModuleCopy.value.branches) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
branches: newModuleCopy.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? replaceModuleInFlow(branch.modules, id, newModule) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (newModuleCopy.value.default) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
default: replaceModuleInFlow(newModuleCopy.value.default, id, newModule)
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'branchall') {
|
||||
if (newModuleCopy.value.branches) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
branches: newModuleCopy.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules ? replaceModuleInFlow(branch.modules, id, newModule) : []
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (newModuleCopy.value.type === 'aiagent') {
|
||||
// Replace tool in AI agent's tools array
|
||||
if (newModuleCopy.value.tools) {
|
||||
newModuleCopy.value = {
|
||||
...newModuleCopy.value,
|
||||
tools: newModuleCopy.value.tools.map((tool) =>
|
||||
tool.id === id
|
||||
? { id, summary: newModule.summary, value: newModule.value as any }
|
||||
: tool
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newModuleCopy
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@ import type {
|
||||
ChatCompletionMessageFunctionToolCall,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/chat/completions.mjs'
|
||||
|
||||
/**
|
||||
* Special module IDs used throughout the flow system
|
||||
*/
|
||||
export const SPECIAL_MODULE_IDS = {
|
||||
/** The flow input schema node */
|
||||
INPUT: 'Input',
|
||||
/** The preprocessor module that runs before the flow */
|
||||
PREPROCESSOR: 'preprocessor',
|
||||
/** The failure handler module */
|
||||
FAILURE: 'failure'
|
||||
} as const
|
||||
import { get } from 'svelte/store'
|
||||
import type { CodePieceElement, ContextElement, FlowModuleCodePieceElement } from './context'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -11,7 +23,6 @@ import type { FunctionParameters } from 'openai/resources/shared.mjs'
|
||||
import { z } from 'zod'
|
||||
import { ScriptService, JobService, type CompletedJob, type FlowModule } from '$lib/gen'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import YAML from 'yaml'
|
||||
import { getCurrentModel } from '$lib/aiStore'
|
||||
|
||||
// Prettify function for code arguments - extracts and formats code from JSON
|
||||
@@ -45,9 +56,91 @@ function prettifyCodeArguments(content: string): string {
|
||||
return codeContent
|
||||
}
|
||||
|
||||
// Prettify function for set_module_code - extracts code from moduleId/code JSON
|
||||
function prettifySetModuleCode(content: string): string {
|
||||
let codeContent = content
|
||||
|
||||
if (typeof content === 'string' && content.trim().startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
if (parsed.code) {
|
||||
codeContent = parsed.code
|
||||
}
|
||||
} catch {
|
||||
// If JSON is incomplete during streaming, try to extract code property manually
|
||||
const codeMatch = content.match(/"code"\s*:\s*"([\s\S]*?)(?:"\s*}?\s*$|$)/)
|
||||
if (codeMatch) {
|
||||
codeContent = codeMatch[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert escape sequences
|
||||
codeContent = codeContent.replace(/\\n/g, '\n')
|
||||
codeContent = codeContent.replace(/\\t/g, '\t')
|
||||
codeContent = codeContent.replace(/\\"/g, '"')
|
||||
codeContent = codeContent.replace(/\\\\/g, '\\')
|
||||
|
||||
return codeContent
|
||||
}
|
||||
|
||||
// Prettify function for module value JSON - extracts the 'value' property and formats it
|
||||
function prettifyModuleValue(content: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
// Extract just the 'value' property (the actual module definition)
|
||||
if (parsed.value) {
|
||||
return JSON.stringify(parsed.value, null, 2)
|
||||
}
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
// If JSON is incomplete during streaming, try to extract the value property manually
|
||||
const valueMatch = content.match(/"value"\s*:\s*(\{[\s\S]*)$/)
|
||||
if (valueMatch) {
|
||||
let valueContent = valueMatch[1]
|
||||
// Try to parse and format the extracted value
|
||||
try {
|
||||
// Find the matching closing brace for the value object
|
||||
let braceCount = 0
|
||||
let endIndex = 0
|
||||
for (let i = 0; i < valueContent.length; i++) {
|
||||
if (valueContent[i] === '{') braceCount++
|
||||
else if (valueContent[i] === '}') braceCount--
|
||||
if (braceCount === 0) {
|
||||
endIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if (endIndex > 0) {
|
||||
const valueJson = valueContent.substring(0, endIndex)
|
||||
const parsed = JSON.parse(valueJson)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, just unescape and return the extracted value content
|
||||
valueContent = valueContent.replace(/\\n/g, '\n')
|
||||
valueContent = valueContent.replace(/\\t/g, '\t')
|
||||
valueContent = valueContent.replace(/\\"/g, '"')
|
||||
valueContent = valueContent.replace(/\\\\/g, '\\')
|
||||
return valueContent
|
||||
}
|
||||
}
|
||||
// Fallback: just unescape and return
|
||||
let result = content
|
||||
result = result.replace(/\\n/g, '\n')
|
||||
result = result.replace(/\\t/g, '\t')
|
||||
result = result.replace(/\\"/g, '"')
|
||||
result = result.replace(/\\\\/g, '\\')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Map of tool names to their prettify functions
|
||||
export const TOOL_PRETTIFY_MAP: Record<string, (content: string) => string> = {
|
||||
edit_code: prettifyCodeArguments
|
||||
edit_code: prettifyCodeArguments,
|
||||
set_module_code: prettifySetModuleCode,
|
||||
add_module: prettifyModuleValue,
|
||||
modify_module: prettifyModuleValue
|
||||
}
|
||||
|
||||
export interface ContextStringResult {
|
||||
@@ -148,7 +241,7 @@ export function applyCodePiecesToFlowModules(
|
||||
}
|
||||
}
|
||||
|
||||
return YAML.stringify(modifiedModules)
|
||||
return JSON.stringify(modifiedModules, null, 2)
|
||||
}
|
||||
|
||||
export function buildContextString(selectedContext: ContextElement[]): string {
|
||||
@@ -363,7 +456,11 @@ export async function processToolCall<T>({
|
||||
error: 'An error occurred while calling the tool'
|
||||
})
|
||||
const errorMessage =
|
||||
typeof err === 'string' ? err : 'An error occurred while calling the tool'
|
||||
typeof err === 'object' && 'message' in err
|
||||
? err.message
|
||||
: typeof err === 'string'
|
||||
? err
|
||||
: 'An error occurred while calling the tool'
|
||||
result = `Error while calling tool: ${errorMessage}`
|
||||
}
|
||||
const toAdd = {
|
||||
|
||||
@@ -244,34 +244,38 @@ export async function fetchAvailableModels(
|
||||
}
|
||||
|
||||
export function getModelMaxTokens(provider: AIProvider, model: string) {
|
||||
if (model.startsWith('gpt-5')) {
|
||||
if (model.includes('gpt-5')) {
|
||||
return 128000
|
||||
} else if ((provider === 'azure_openai' || provider === 'openai') && model.startsWith('o')) {
|
||||
return 100000
|
||||
} else if (model.startsWith('claude-sonnet') || model.startsWith('gemini-2.5')) {
|
||||
} else if (
|
||||
model.includes('claude-sonnet') ||
|
||||
model.includes('gemini-2.5') ||
|
||||
model.includes('claude-haiku')
|
||||
) {
|
||||
return 64000
|
||||
} else if (model.startsWith('gpt-4.1')) {
|
||||
} else if (model.includes('gpt-4.1')) {
|
||||
return 32768
|
||||
} else if (model.startsWith('claude-opus')) {
|
||||
} else if (model.includes('claude-opus')) {
|
||||
return 32000
|
||||
} else if (model.startsWith('gpt-4o') || model.startsWith('codestral')) {
|
||||
} else if (model.includes('gpt-4o') || model.includes('codestral')) {
|
||||
return 16384
|
||||
} else if (model.startsWith('gpt-4-turbo') || model.startsWith('gpt-3.5')) {
|
||||
} else if (model.includes('gpt-4-turbo') || model.includes('gpt-3.5')) {
|
||||
return 4096
|
||||
}
|
||||
return 8192
|
||||
}
|
||||
|
||||
export function getModelContextWindow(model: string) {
|
||||
if (model.startsWith('gpt-4.1') || model.startsWith('gemini')) {
|
||||
if (model.includes('gpt-4.1') || model.includes('gemini')) {
|
||||
return 1000000
|
||||
} else if (model.startsWith('gpt-5')) {
|
||||
} else if (model.includes('gpt-5')) {
|
||||
return 400000
|
||||
} else if (model.startsWith('gpt-4o') || model.startsWith('llama-3.3')) {
|
||||
} else if (model.includes('gpt-4o') || model.includes('llama-3.3')) {
|
||||
return 128000
|
||||
} else if (model.startsWith('claude') || model.startsWith('o4-mini') || model.startsWith('o3')) {
|
||||
} else if (model.includes('claude') || model.includes('o4-mini') || model.includes('o3')) {
|
||||
return 200000
|
||||
} else if (model.startsWith('codestral')) {
|
||||
} else if (model.includes('codestral')) {
|
||||
return 32000
|
||||
} else {
|
||||
return 128000
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
aiChatManager.flowOptions = undefined
|
||||
aiChatManager.saveAndClear()
|
||||
aiChatManager.changeMode(AIMode.NAVIGATOR)
|
||||
})
|
||||
</script>
|
||||
@@ -213,11 +214,12 @@
|
||||
{suspendStatus}
|
||||
onOpenDetails={onOpenPreview}
|
||||
{previewOpen}
|
||||
{flowModuleSchemaMap}
|
||||
/>
|
||||
{/if}
|
||||
</Pane>
|
||||
{#if !disableAi}
|
||||
<FlowAIChat {flowModuleSchemaMap} />
|
||||
<FlowAIChat {flowModuleSchemaMap} {onTestFlow} />
|
||||
{/if}
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
|
||||
onOpenDetails?: () => void
|
||||
previewOpen?: boolean
|
||||
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -52,7 +53,8 @@
|
||||
isOwner,
|
||||
suspendStatus,
|
||||
onOpenDetails,
|
||||
previewOpen = false
|
||||
previewOpen = false,
|
||||
flowModuleSchemaMap = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const {
|
||||
@@ -72,6 +74,7 @@
|
||||
|
||||
const { showCaptureHint, triggersState, triggersCount } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
function checkDup(modules: FlowModule[]): string | undefined {
|
||||
let seenModules: string[] = []
|
||||
for (const m of modules) {
|
||||
@@ -104,6 +107,7 @@
|
||||
on:applyArgs
|
||||
{onTestFlow}
|
||||
{previewOpen}
|
||||
{flowModuleSchemaMap}
|
||||
/>
|
||||
{:else if selectedId === 'Result'}
|
||||
<FlowResult {noEditor} {job} {isOwner} {suspendStatus} {onOpenDetails} />
|
||||
|
||||
@@ -55,9 +55,16 @@
|
||||
disabled: boolean
|
||||
onTestFlow?: (conversationId?: string) => Promise<string | undefined>
|
||||
previewOpen: boolean
|
||||
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
|
||||
}
|
||||
|
||||
let { noEditor, disabled, onTestFlow, previewOpen }: Props = $props()
|
||||
let {
|
||||
noEditor,
|
||||
disabled,
|
||||
onTestFlow,
|
||||
previewOpen,
|
||||
flowModuleSchemaMap = undefined
|
||||
}: Props = $props()
|
||||
const {
|
||||
flowStore,
|
||||
flowStateStore,
|
||||
@@ -68,6 +75,12 @@
|
||||
flowInputEditorState
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
// Get diffManager from the graph
|
||||
const diffManager = $derived(flowModuleSchemaMap?.getDiffManager())
|
||||
|
||||
// Use pending schema from diffManager when in diff mode, otherwise use flowStore
|
||||
const effectiveSchema = $derived(diffManager?.currentInputSchema ?? flowStore.val.schema)
|
||||
|
||||
let chatInputEnabled = $state(Boolean(flowStore.val.value?.chat_input_enabled))
|
||||
let shouldUseStreaming = $derived.by(() => {
|
||||
const modules = flowStore.val.value?.modules
|
||||
@@ -447,19 +460,22 @@
|
||||
value: {
|
||||
type: 'aiagent',
|
||||
tools: [],
|
||||
input_transforms: Object.keys(AI_AGENT_SCHEMA.properties ?? {}).reduce((accu, key) => {
|
||||
if (key === 'user_message') {
|
||||
accu[key] = { type: 'javascript', expr: 'flow_input.user_message' }
|
||||
} else if (key === 'messages_context_length') {
|
||||
accu[key] = { type: 'static', value: 10 }
|
||||
} else {
|
||||
accu[key] = {
|
||||
type: 'static',
|
||||
value: undefined
|
||||
input_transforms: Object.keys(AI_AGENT_SCHEMA.properties ?? {}).reduce(
|
||||
(accu, key) => {
|
||||
if (key === 'user_message') {
|
||||
accu[key] = { type: 'javascript', expr: 'flow_input.user_message' }
|
||||
} else if (key === 'messages_context_length') {
|
||||
accu[key] = { type: 'static', value: 10 }
|
||||
} else {
|
||||
accu[key] = {
|
||||
type: 'static',
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
return accu
|
||||
}, {})
|
||||
return accu
|
||||
},
|
||||
{} as AiAgent['input_transforms']
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -806,7 +822,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-4 border-b">
|
||||
<FlowInputViewer schema={flowStore.val.schema} />
|
||||
<FlowInputViewer schema={effectiveSchema} />
|
||||
</div>
|
||||
{/if}
|
||||
</FlowCard>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
import { expect } from 'vitest'
|
||||
import type {
|
||||
FlowValue,
|
||||
FlowModule,
|
||||
RawScript,
|
||||
Identity,
|
||||
ForloopFlow,
|
||||
WhileloopFlow,
|
||||
BranchOne,
|
||||
BranchAll
|
||||
} from '$lib/gen'
|
||||
import type { ExtendedOpenFlow } from './types'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
|
||||
// ============================================================================
|
||||
// Module Creation Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a minimal RawScript module for testing
|
||||
*/
|
||||
export function createRawScriptModule(id: string, content: string): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content,
|
||||
language: 'bun',
|
||||
input_transforms: {}
|
||||
} as RawScript
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal Identity module (useful for type-change tests)
|
||||
*/
|
||||
export function createIdentityModule(id: string): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: 'identity'
|
||||
} as Identity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ForloopFlow module with nested modules
|
||||
*/
|
||||
export function createForloopModule(id: string, nestedModules: FlowModule[]): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: 'forloopflow',
|
||||
modules: nestedModules,
|
||||
iterator: { type: 'javascript', expr: '[1,2,3]' },
|
||||
skip_failures: false
|
||||
} as ForloopFlow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WhileloopFlow module with nested modules
|
||||
*/
|
||||
export function createWhileloopModule(id: string, nestedModules: FlowModule[]): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: 'whileloopflow',
|
||||
modules: nestedModules,
|
||||
skip_failures: false
|
||||
} as WhileloopFlow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BranchOne module with default and conditional branches
|
||||
*/
|
||||
export function createBranchOneModule(
|
||||
id: string,
|
||||
defaultModules: FlowModule[],
|
||||
branches: { expr: string; modules: FlowModule[] }[]
|
||||
): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: 'branchone',
|
||||
default: defaultModules,
|
||||
branches: branches.map((b) => ({ expr: b.expr, modules: b.modules }))
|
||||
} as BranchOne
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BranchAll module with parallel branches
|
||||
*/
|
||||
export function createBranchAllModule(id: string, branches: { modules: FlowModule[] }[]): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: 'branchall',
|
||||
branches: branches.map((b) => ({ modules: b.modules }))
|
||||
} as BranchAll
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Flow Creation Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a FlowValue with the given modules
|
||||
*/
|
||||
export function createFlow(modules: FlowModule[]): FlowValue {
|
||||
return { modules }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FlowValue with optional special modules (failure_module, preprocessor_module)
|
||||
*/
|
||||
export function createFlowWithSpecialModules(options: {
|
||||
modules?: FlowModule[]
|
||||
failure_module?: FlowModule
|
||||
preprocessor_module?: FlowModule
|
||||
}): FlowValue {
|
||||
return {
|
||||
modules: options.modules ?? [],
|
||||
...(options.failure_module && { failure_module: options.failure_module }),
|
||||
...(options.preprocessor_module && { preprocessor_module: options.preprocessor_module })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a FlowValue in an ExtendedOpenFlow for manager tests
|
||||
*/
|
||||
export function createExtendedOpenFlow(
|
||||
flowValue: FlowValue,
|
||||
schema?: Record<string, unknown>
|
||||
): ExtendedOpenFlow {
|
||||
return { value: flowValue, summary: '', schema }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal StateStore for manager tests
|
||||
*/
|
||||
export function createFlowStore(flow: ExtendedOpenFlow): StateStore<ExtendedOpenFlow> {
|
||||
return { val: flow }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Deep clones an object via JSON serialization
|
||||
*/
|
||||
export function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that modules appear in the exact order specified.
|
||||
* Also implicitly verifies the count of modules.
|
||||
*/
|
||||
export function expectModuleOrder(modules: FlowModule[], expectedIds: string[]): void {
|
||||
expect(modules.map((m) => m.id)).toEqual(expectedIds)
|
||||
}
|
||||
@@ -1,17 +1,59 @@
|
||||
import type { FlowModule, FlowValue } from '$lib/gen'
|
||||
import { dfs } from './dfs'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { AIModuleAction } from '../copilot/chat/flow/core'
|
||||
|
||||
/** Prefix added to module IDs when the original module coexists with a replacement */
|
||||
export const DUPLICATE_MODULE_PREFIX = 'old__'
|
||||
|
||||
/** Prefix added to new module IDs when restoring original during type change rejection */
|
||||
export const NEW_MODULE_PREFIX = 'new__'
|
||||
|
||||
/**
|
||||
* Action types for flow module changes during diff tracking
|
||||
* - added: Module was added to the flow
|
||||
* - modified: Module content was changed
|
||||
* - removed: Module was deleted from the flow
|
||||
* - shadowed: Module is shown as removed (visualization mode)
|
||||
*/
|
||||
export type AIModuleAction = 'added' | 'modified' | 'removed' | 'shadowed' | undefined
|
||||
|
||||
/**
|
||||
* Tracks the action performed on a module and whether it requires user approval
|
||||
*/
|
||||
export type ModuleActionInfo = {
|
||||
action: AIModuleAction
|
||||
/** Whether this change is pending user approval (accept/reject) */
|
||||
pending: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a FlowModule for comparison by removing properties that
|
||||
* should be ignored when determining if a module has changed.
|
||||
* Specifically, removes empty `assets` arrays since their presence/absence
|
||||
* is not a meaningful difference.
|
||||
*/
|
||||
function normalizeModuleForComparison(module: FlowModule): FlowModule {
|
||||
const normalized = { ...module }
|
||||
if ('value' in normalized && normalized.value && typeof normalized.value === 'object') {
|
||||
const value = { ...normalized.value } as Record<string, unknown>
|
||||
// Remove empty assets array - it's not a meaningful difference
|
||||
if (Array.isArray(value.assets) && value.assets.length === 0) {
|
||||
delete value.assets
|
||||
}
|
||||
normalized.value = value as FlowModule['value']
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete diff result with action maps and merged flow
|
||||
*/
|
||||
export type FlowTimeline = {
|
||||
/** Actions for modules in the before flow */
|
||||
beforeActions: Record<string, AIModuleAction>
|
||||
beforeActions: Record<string, ModuleActionInfo>
|
||||
|
||||
/** Actions for modules in the after flow (adjusted based on display mode) */
|
||||
afterActions: Record<string, AIModuleAction>
|
||||
afterActions: Record<string, ModuleActionInfo>
|
||||
|
||||
/** The merged flow containing both after modules and removed modules properly nested */
|
||||
mergedFlow: FlowValue
|
||||
@@ -29,40 +71,56 @@ export type FlowTimeline = {
|
||||
*/
|
||||
export function computeFlowModuleDiff(
|
||||
beforeFlow: FlowValue,
|
||||
afterFlow: FlowValue
|
||||
): { beforeActions: Record<string, AIModuleAction>; afterActions: Record<string, AIModuleAction> } {
|
||||
const beforeActions: Record<string, AIModuleAction> = {}
|
||||
const afterActions: Record<string, AIModuleAction> = {}
|
||||
afterFlow: FlowValue,
|
||||
options: { markAsPending: boolean } = { markAsPending: false }
|
||||
): {
|
||||
beforeActions: Record<string, ModuleActionInfo>
|
||||
afterActions: Record<string, ModuleActionInfo>
|
||||
} {
|
||||
const beforeActions: Record<string, ModuleActionInfo> = {}
|
||||
const afterActions: Record<string, ModuleActionInfo> = {}
|
||||
|
||||
// Get all modules from both flows using dfs
|
||||
const beforeModules = getAllModulesMap(beforeFlow)
|
||||
const afterModules = getAllModulesMap(afterFlow)
|
||||
// Get all modules with their locations from both flows
|
||||
const beforeModulesWithLoc = getAllModulesWithLocation(beforeFlow)
|
||||
const afterModulesWithLoc = getAllModulesWithLocation(afterFlow)
|
||||
|
||||
// Find all module IDs
|
||||
const allModuleIds = new Set([...beforeModules.keys(), ...afterModules.keys()])
|
||||
const allModuleIds = new Set([...beforeModulesWithLoc.keys(), ...afterModulesWithLoc.keys()])
|
||||
|
||||
for (const moduleId of allModuleIds) {
|
||||
const beforeModule = beforeModules.get(moduleId)
|
||||
const afterModule = afterModules.get(moduleId)
|
||||
const beforeEntry = beforeModulesWithLoc.get(moduleId)
|
||||
const afterEntry = afterModulesWithLoc.get(moduleId)
|
||||
|
||||
if (!beforeModule && afterModule) {
|
||||
if (!beforeEntry && afterEntry) {
|
||||
// Module exists in after but not before -> added
|
||||
afterActions[moduleId] = 'added'
|
||||
} else if (beforeModule && !afterModule) {
|
||||
afterActions[moduleId] = { action: 'added', pending: options.markAsPending }
|
||||
} else if (beforeEntry && !afterEntry) {
|
||||
// Module exists in before but not after -> removed
|
||||
beforeActions[moduleId] = 'removed'
|
||||
afterActions[moduleId] = 'shadowed'
|
||||
} else if (beforeModule && afterModule) {
|
||||
// Module exists in both -> check type and content
|
||||
const typeChanged = beforeModule.value.type !== afterModule.value.type
|
||||
if (typeChanged) {
|
||||
// Type changed -> treat as removed + added
|
||||
beforeActions[moduleId] = 'removed'
|
||||
afterActions[moduleId] = 'added'
|
||||
} else if (!deepEqual(beforeModule, afterModule)) {
|
||||
// Same type but different content -> modified
|
||||
beforeActions[moduleId] = 'modified'
|
||||
afterActions[moduleId] = 'modified'
|
||||
beforeActions[moduleId] = { action: 'removed', pending: options.markAsPending }
|
||||
afterActions[moduleId] = { action: 'shadowed', pending: options.markAsPending }
|
||||
} else if (beforeEntry && afterEntry) {
|
||||
// Module exists in both -> check location first, then type and content
|
||||
if (!locationsEqual(beforeEntry.location, afterEntry.location)) {
|
||||
// Location changed -> treat as removed from old + added at new
|
||||
beforeActions[moduleId] = { action: 'removed', pending: options.markAsPending }
|
||||
afterActions[moduleId] = { action: 'added', pending: options.markAsPending }
|
||||
} else {
|
||||
// Same location -> check type and content
|
||||
const typeChanged = beforeEntry.module.value.type !== afterEntry.module.value.type
|
||||
if (typeChanged) {
|
||||
// Type changed -> treat as removed + added
|
||||
beforeActions[moduleId] = { action: 'removed', pending: options.markAsPending }
|
||||
afterActions[moduleId] = { action: 'added', pending: options.markAsPending }
|
||||
} else if (
|
||||
!deepEqual(
|
||||
normalizeModuleForComparison(beforeEntry.module),
|
||||
normalizeModuleForComparison(afterEntry.module)
|
||||
)
|
||||
) {
|
||||
// Same type but different content -> modified
|
||||
beforeActions[moduleId] = { action: 'modified', pending: options.markAsPending }
|
||||
afterActions[moduleId] = { action: 'modified', pending: options.markAsPending }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,10 +155,81 @@ function getAllModulesMap(flow: FlowValue): Map<string, FlowModule> {
|
||||
return moduleMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a module along with its location in the flow
|
||||
*/
|
||||
type ModuleWithLocation = {
|
||||
module: FlowModule
|
||||
location: ModuleParentLocation
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get all modules from a flow as a Map with their locations
|
||||
*/
|
||||
function getAllModulesWithLocation(flow: FlowValue): Map<string, ModuleWithLocation> {
|
||||
const result = new Map<string, ModuleWithLocation>()
|
||||
const allModules = dfs(flow.modules ?? [], (m) => m)
|
||||
|
||||
for (const module of allModules) {
|
||||
if (module?.id) {
|
||||
const location = findModuleParent(flow, module.id)
|
||||
if (location) {
|
||||
result.set(module.id, { module, location })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add special modules
|
||||
if (flow.failure_module?.id) {
|
||||
result.set(flow.failure_module.id, {
|
||||
module: flow.failure_module,
|
||||
location: { type: 'failure', index: -1 }
|
||||
})
|
||||
}
|
||||
if (flow.preprocessor_module?.id) {
|
||||
result.set(flow.preprocessor_module.id, {
|
||||
module: flow.preprocessor_module,
|
||||
location: { type: 'preprocessor', index: -1 }
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two module locations for equality.
|
||||
* Two locations are equal if they refer to the same parent container.
|
||||
* Index within the container is not considered (modules can be reordered).
|
||||
*/
|
||||
export function locationsEqual(a: ModuleParentLocation | null, b: ModuleParentLocation | null): boolean {
|
||||
if (!a || !b) return a === b
|
||||
if (a.type !== b.type) return false
|
||||
|
||||
switch (a.type) {
|
||||
case 'root':
|
||||
case 'failure':
|
||||
case 'preprocessor':
|
||||
return true // Same type is enough (index doesn't matter for location equality)
|
||||
case 'forloop':
|
||||
case 'whileloop':
|
||||
case 'aiagent':
|
||||
return a.parentId === (b as typeof a).parentId
|
||||
case 'branchone-default':
|
||||
return a.parentId === (b as typeof a).parentId
|
||||
case 'branchone-branch':
|
||||
case 'branchall-branch':
|
||||
return (
|
||||
a.parentId === (b as typeof a).parentId && a.branchIndex === (b as typeof a).branchIndex
|
||||
)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the parent location of a module
|
||||
*/
|
||||
type ModuleParentLocation =
|
||||
export type ModuleParentLocation =
|
||||
| { type: 'root'; index: number }
|
||||
| { type: 'forloop' | 'whileloop'; parentId: string; index: number }
|
||||
| { type: 'branchone-default'; parentId: string; index: number }
|
||||
@@ -113,7 +242,7 @@ type ModuleParentLocation =
|
||||
/**
|
||||
* Finds the parent location of a module in a flow
|
||||
*/
|
||||
function findModuleParent(flow: FlowValue, moduleId: string): ModuleParentLocation | null {
|
||||
export function findModuleParent(flow: FlowValue, moduleId: string): ModuleParentLocation | null {
|
||||
// Check special modules
|
||||
if (flow.failure_module?.id === moduleId) {
|
||||
return { type: 'failure', index: -1 }
|
||||
@@ -210,11 +339,62 @@ function cloneModule(module: FlowModule): FlowModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends a prefix to a module's ID to avoid collisions
|
||||
* Prepends a prefix to a module's ID and all nested child module IDs to avoid collisions
|
||||
*/
|
||||
function prependModuleId(module: FlowModule, prefix: string): FlowModule {
|
||||
const newModule = cloneModule(module)
|
||||
newModule.id = prefix + newModule.id
|
||||
|
||||
// Recursively prefix nested module IDs
|
||||
if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') {
|
||||
newModule.value.modules = newModule.value.modules.map((m) => prependModuleId(m, prefix))
|
||||
} else if (newModule.value.type === 'branchone') {
|
||||
newModule.value.default = newModule.value.default.map((m) => prependModuleId(m, prefix))
|
||||
newModule.value.branches = newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules.map((m) => prependModuleId(m, prefix))
|
||||
}))
|
||||
} else if (newModule.value.type === 'branchall') {
|
||||
newModule.value.branches = newModule.value.branches.map((branch) => ({
|
||||
...branch,
|
||||
modules: branch.modules.map((m) => prependModuleId(m, prefix))
|
||||
}))
|
||||
} else if (newModule.value.type === 'aiagent' && newModule.value.tools) {
|
||||
// Handle aiagent tools - only prefix FlowModule tools, not MCP tools
|
||||
newModule.value.tools = newModule.value.tools.map((tool) => {
|
||||
// MCP tools have tool_type: 'mcp', FlowModule tools have tool_type: 'flowmodule' or undefined
|
||||
if (tool.value.tool_type === 'mcp') {
|
||||
return tool // MCP tools don't have nested module IDs
|
||||
}
|
||||
// For FlowModule tools, prefix the ID and recurse
|
||||
const prefixedTool = {
|
||||
...tool,
|
||||
id: prefix + tool.id
|
||||
}
|
||||
// If the tool has nested modules (it's a container type), recurse
|
||||
const innerValue = tool.value as FlowModule['value']
|
||||
if (innerValue.type === 'forloopflow' || innerValue.type === 'whileloopflow') {
|
||||
;(prefixedTool.value as any).modules = (innerValue as any).modules.map((m: FlowModule) =>
|
||||
prependModuleId(m, prefix)
|
||||
)
|
||||
} else if (innerValue.type === 'branchone') {
|
||||
;(prefixedTool.value as any).default = (innerValue as any).default.map((m: FlowModule) =>
|
||||
prependModuleId(m, prefix)
|
||||
)
|
||||
;(prefixedTool.value as any).branches = (innerValue as any).branches.map((branch: any) => ({
|
||||
...branch,
|
||||
modules: branch.modules.map((m: FlowModule) => prependModuleId(m, prefix))
|
||||
}))
|
||||
} else if (innerValue.type === 'branchall') {
|
||||
;(prefixedTool.value as any).branches = (innerValue as any).branches.map((branch: any) => ({
|
||||
...branch,
|
||||
modules: branch.modules.map((m: FlowModule) => prependModuleId(m, prefix))
|
||||
}))
|
||||
}
|
||||
return prefixedTool
|
||||
})
|
||||
}
|
||||
|
||||
return newModule
|
||||
}
|
||||
|
||||
@@ -264,28 +444,122 @@ function getAllModuleIds(flow: FlowValue): Set<string> {
|
||||
return ids
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the merged flow for duplicate IDs and prefixes duplicates with 'old__'.
|
||||
* This handles the case where a module is moved from one location to another -
|
||||
* both the old and new versions end up in the merged flow with the same ID.
|
||||
*/
|
||||
function fixDuplicateIds(merged: FlowValue, beforeFlow: FlowValue): void {
|
||||
const seenIds = new Set<string>()
|
||||
const beforeModulesMap = getAllModulesMap(beforeFlow)
|
||||
|
||||
// Process a single module - returns the (possibly prefixed) module
|
||||
function processModule(module: FlowModule): FlowModule {
|
||||
let result = module
|
||||
|
||||
if (seenIds.has(module.id)) {
|
||||
// Duplicate found! Check if this one exists in beforeFlow
|
||||
const beforeModule = beforeModulesMap.get(module.id)
|
||||
if (beforeModule) {
|
||||
// This is the "old" version - prefix it and all its children
|
||||
result = prependModuleId(module, DUPLICATE_MODULE_PREFIX)
|
||||
}
|
||||
} else {
|
||||
seenIds.add(module.id)
|
||||
}
|
||||
|
||||
// Recurse into nested modules (use result which may be prefixed)
|
||||
processNestedModules(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Process nested modules in-place
|
||||
function processNestedModules(module: FlowModule): void {
|
||||
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
|
||||
module.value.modules = module.value.modules.map((m) => processModule(m))
|
||||
} else if (module.value.type === 'branchone') {
|
||||
module.value.default = module.value.default.map((m) => processModule(m))
|
||||
for (const branch of module.value.branches) {
|
||||
branch.modules = branch.modules.map((m) => processModule(m))
|
||||
}
|
||||
} else if (module.value.type === 'branchall') {
|
||||
for (const branch of module.value.branches) {
|
||||
branch.modules = branch.modules.map((m) => processModule(m))
|
||||
}
|
||||
} else if (module.value.type === 'aiagent' && module.value.tools) {
|
||||
// For aiagent tools, we need to track IDs of FlowModule tools
|
||||
for (const tool of module.value.tools) {
|
||||
if (tool.value.tool_type !== 'mcp') {
|
||||
if (seenIds.has(tool.id)) {
|
||||
// Can't easily prefix in-place here, but aiagent tools rarely move
|
||||
// The main use case is regular modules moving in/out of loops/branches
|
||||
} else {
|
||||
seenIds.add(tool.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process root modules
|
||||
if (merged.modules) {
|
||||
merged.modules = merged.modules.map((m) => processModule(m))
|
||||
}
|
||||
|
||||
// Process special modules
|
||||
if (merged.failure_module) {
|
||||
if (seenIds.has(merged.failure_module.id)) {
|
||||
const beforeModule = beforeModulesMap.get(merged.failure_module.id)
|
||||
if (beforeModule) {
|
||||
merged.failure_module = prependModuleId(merged.failure_module, DUPLICATE_MODULE_PREFIX)
|
||||
}
|
||||
} else {
|
||||
seenIds.add(merged.failure_module.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (merged.preprocessor_module) {
|
||||
if (seenIds.has(merged.preprocessor_module.id)) {
|
||||
const beforeModule = beforeModulesMap.get(merged.preprocessor_module.id)
|
||||
if (beforeModule) {
|
||||
merged.preprocessor_module = prependModuleId(
|
||||
merged.preprocessor_module,
|
||||
DUPLICATE_MODULE_PREFIX
|
||||
)
|
||||
}
|
||||
} else {
|
||||
seenIds.add(merged.preprocessor_module.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs the merged flow with removed modules properly nested
|
||||
*/
|
||||
function reconstructMergedFlow(
|
||||
afterFlow: FlowValue,
|
||||
beforeFlow: FlowValue,
|
||||
beforeActions: Record<string, AIModuleAction>
|
||||
beforeActions: Record<string, ModuleActionInfo>
|
||||
): FlowValue {
|
||||
// Deep clone afterFlow to avoid mutation
|
||||
const merged: FlowValue = JSON.parse(JSON.stringify(afterFlow))
|
||||
|
||||
// Get all removed/shadowed modules from beforeFlow
|
||||
const removedModules = Object.entries(beforeActions)
|
||||
.filter(([_, action]) => action === 'removed' || action === 'shadowed')
|
||||
.filter(([_, action]) => action.action === 'removed' || action.action === 'shadowed')
|
||||
.map(([id]) => id)
|
||||
|
||||
// Create a Set for faster lookup
|
||||
const removedModulesSet = new Set(removedModules)
|
||||
|
||||
// Cache beforeFlow modules map and merged IDs to avoid recomputing in the loop
|
||||
const beforeModulesMap = getAllModulesMap(beforeFlow)
|
||||
const mergedIds = getAllModuleIds(merged)
|
||||
|
||||
// For each removed module, find its parent and insert it
|
||||
for (const removedId of removedModules) {
|
||||
const beforeModule = getAllModulesMap(beforeFlow).get(removedId)
|
||||
const beforeModule = beforeModulesMap.get(removedId)
|
||||
if (!beforeModule) continue
|
||||
|
||||
const parentLocation = findModuleParent(beforeFlow, removedId)
|
||||
@@ -308,12 +582,14 @@ function reconstructMergedFlow(
|
||||
|
||||
// Check for ID collision - this happens when a module type changed
|
||||
// In this case, the new module is already in the merged flow as 'added'
|
||||
// We need to prepend "__" to the removed module's ID so both can coexist
|
||||
const existingIds = getAllModuleIds(merged)
|
||||
if (existingIds.has(clonedModule.id)) {
|
||||
clonedModule = prependModuleId(clonedModule, '__')
|
||||
// We prepend the duplicate prefix to the removed module's ID so both can coexist
|
||||
if (mergedIds.has(clonedModule.id)) {
|
||||
clonedModule = prependModuleId(clonedModule, DUPLICATE_MODULE_PREFIX)
|
||||
}
|
||||
|
||||
// Track the newly added module ID
|
||||
mergedIds.add(clonedModule.id)
|
||||
|
||||
// Insert based on parent location
|
||||
if (parentLocation.type === 'failure') {
|
||||
merged.failure_module = clonedModule
|
||||
@@ -335,6 +611,10 @@ function reconstructMergedFlow(
|
||||
}
|
||||
}
|
||||
|
||||
// Post-process: fix any duplicate IDs that may have been created
|
||||
// This handles the case where a module moved from one location to another
|
||||
fixDuplicateIds(merged, beforeFlow)
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -389,11 +669,17 @@ function insertIntoNestedParent(
|
||||
|
||||
// Find the parent module in merged flow
|
||||
const parentModule = findModuleById(merged, parentLocation.parentId)
|
||||
if (!parentModule) return
|
||||
if (!parentModule) {
|
||||
console.warn('Parent module not found', parentLocation)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the before parent to know original ordering
|
||||
const beforeParent = findModuleById(beforeFlow, parentLocation.parentId)
|
||||
if (!beforeParent) return
|
||||
if (!beforeParent) {
|
||||
console.warn('Before parent module not found', parentLocation)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert based on type
|
||||
if (parentLocation.type === 'forloop' && parentModule.value.type === 'forloopflow') {
|
||||
@@ -430,9 +716,24 @@ function insertIntoNestedParent(
|
||||
parentLocation.type === 'branchone-branch' &&
|
||||
parentModule.value.type === 'branchone'
|
||||
) {
|
||||
const branch = parentModule.value.branches[parentLocation.branchIndex]
|
||||
let branch = parentModule.value.branches[parentLocation.branchIndex]
|
||||
const beforeBranch = (beforeParent.value as any).branches?.[parentLocation.branchIndex]
|
||||
|
||||
// If the branch doesn't exist (entire branch was removed), recreate it from beforeFlow
|
||||
if (!branch && beforeBranch) {
|
||||
// Ensure we have enough branch slots
|
||||
while (parentModule.value.branches.length <= parentLocation.branchIndex) {
|
||||
parentModule.value.branches.push({ expr: '', modules: [] })
|
||||
}
|
||||
// Restore the branch with its original expr but empty modules (we'll add them)
|
||||
parentModule.value.branches[parentLocation.branchIndex] = {
|
||||
...beforeBranch,
|
||||
modules: []
|
||||
}
|
||||
branch = parentModule.value.branches[parentLocation.branchIndex]
|
||||
}
|
||||
|
||||
if (branch) {
|
||||
const beforeBranch = (beforeParent.value as any).branches?.[parentLocation.branchIndex]
|
||||
const beforeModules = beforeBranch?.modules ?? []
|
||||
const insertIndex = findBestInsertPosition(
|
||||
branch.modules,
|
||||
@@ -446,9 +747,24 @@ function insertIntoNestedParent(
|
||||
parentLocation.type === 'branchall-branch' &&
|
||||
parentModule.value.type === 'branchall'
|
||||
) {
|
||||
const branch = parentModule.value.branches[parentLocation.branchIndex]
|
||||
let branch = parentModule.value.branches[parentLocation.branchIndex]
|
||||
const beforeBranch = (beforeParent.value as any).branches?.[parentLocation.branchIndex]
|
||||
|
||||
// If the branch doesn't exist (entire branch was removed), recreate it from beforeFlow
|
||||
if (!branch && beforeBranch) {
|
||||
// Ensure we have enough branch slots
|
||||
while (parentModule.value.branches.length <= parentLocation.branchIndex) {
|
||||
parentModule.value.branches.push({ modules: [] })
|
||||
}
|
||||
// Restore the branch with empty modules (we'll add them)
|
||||
parentModule.value.branches[parentLocation.branchIndex] = {
|
||||
...beforeBranch,
|
||||
modules: []
|
||||
}
|
||||
branch = parentModule.value.branches[parentLocation.branchIndex]
|
||||
}
|
||||
|
||||
if (branch) {
|
||||
const beforeBranch = (beforeParent.value as any).branches?.[parentLocation.branchIndex]
|
||||
const beforeModules = beforeBranch?.modules ?? []
|
||||
const insertIndex = findBestInsertPosition(
|
||||
branch.modules,
|
||||
@@ -483,33 +799,36 @@ function findModuleById(flow: FlowValue, moduleId: string): FlowModule | null {
|
||||
* Adjusts the after actions based on display mode and adds entries for prefixed IDs
|
||||
*/
|
||||
function adjustActionsForDisplay(
|
||||
afterActions: Record<string, AIModuleAction>,
|
||||
beforeActions: Record<string, AIModuleAction>,
|
||||
afterActions: Record<string, ModuleActionInfo>,
|
||||
beforeActions: Record<string, ModuleActionInfo>,
|
||||
markRemovedAsShadowed: boolean,
|
||||
mergedFlow: FlowValue
|
||||
): Record<string, AIModuleAction> {
|
||||
const adjusted: Record<string, AIModuleAction> = {}
|
||||
): Record<string, ModuleActionInfo> {
|
||||
const adjusted: Record<string, ModuleActionInfo> = {}
|
||||
|
||||
// Copy all existing actions
|
||||
for (const [id, action] of Object.entries(afterActions)) {
|
||||
if (!markRemovedAsShadowed && action === 'shadowed') {
|
||||
if (!markRemovedAsShadowed && action.action === 'shadowed') {
|
||||
// In unified mode, change 'shadowed' to 'removed' for proper coloring
|
||||
adjusted[id] = 'removed'
|
||||
adjusted[id] = { action: 'removed', pending: action.pending }
|
||||
} else {
|
||||
adjusted[id] = action
|
||||
}
|
||||
}
|
||||
|
||||
// Add entries for prefixed IDs (modules that had type changes or were removed)
|
||||
// These are the old versions that got "__" prepended to their ID
|
||||
// These are the old versions that got the duplicate prefix prepended to their ID
|
||||
const allMergedIds = getAllModuleIds(mergedFlow)
|
||||
for (const id of allMergedIds) {
|
||||
if (id.startsWith('__') && !adjusted[id]) {
|
||||
if (id.startsWith(DUPLICATE_MODULE_PREFIX) && !adjusted[id]) {
|
||||
// This is a prefixed ID for a module that was removed
|
||||
const originalId = id.substring(2)
|
||||
const originalId = id.substring(DUPLICATE_MODULE_PREFIX.length)
|
||||
// Check beforeActions to see if this module was removed
|
||||
if (beforeActions[originalId] === 'removed') {
|
||||
adjusted[id] = markRemovedAsShadowed ? 'shadowed' : 'removed'
|
||||
if (beforeActions[originalId]?.action === 'removed') {
|
||||
adjusted[id] = {
|
||||
action: markRemovedAsShadowed ? 'shadowed' : 'removed',
|
||||
pending: beforeActions[originalId].pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,10 +849,15 @@ function adjustActionsForDisplay(
|
||||
export function buildFlowTimeline(
|
||||
beforeFlow: FlowValue,
|
||||
afterFlow: FlowValue,
|
||||
options: { markRemovedAsShadowed: boolean } = { markRemovedAsShadowed: false }
|
||||
options: { markRemovedAsShadowed: boolean; markAsPending: boolean } = {
|
||||
markRemovedAsShadowed: false,
|
||||
markAsPending: false
|
||||
}
|
||||
): FlowTimeline {
|
||||
// Compute the diff between the two flows
|
||||
const { beforeActions, afterActions } = computeFlowModuleDiff(beforeFlow, afterFlow)
|
||||
const { beforeActions, afterActions } = computeFlowModuleDiff(beforeFlow, afterFlow, {
|
||||
markAsPending: options.markAsPending
|
||||
})
|
||||
|
||||
// Reconstruct merged flow with removed modules properly nested
|
||||
const mergedFlow = reconstructMergedFlow(afterFlow, beforeFlow, beforeActions)
|
||||
@@ -553,6 +877,62 @@ export function buildFlowTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a module into a flow at its correct position based on where it was located in the source flow.
|
||||
* This is useful when restoring a removed module - it finds the correct parent and position.
|
||||
*
|
||||
* @param targetFlow - The flow to insert the module into
|
||||
* @param moduleToInsert - The module to insert
|
||||
* @param sourceFlow - The flow where the module originally existed (to find parent location and ordering)
|
||||
* @param moduleId - The ID of the module being inserted
|
||||
*/
|
||||
export function insertModuleIntoFlow(
|
||||
targetFlow: FlowValue,
|
||||
moduleToInsert: FlowModule,
|
||||
sourceFlow: FlowValue,
|
||||
moduleId: string
|
||||
): void {
|
||||
const parentLocation = findModuleParent(sourceFlow, moduleId)
|
||||
if (!parentLocation) return
|
||||
|
||||
// Handle special modules
|
||||
if (parentLocation.type === 'failure') {
|
||||
targetFlow.failure_module = moduleToInsert
|
||||
return
|
||||
}
|
||||
if (parentLocation.type === 'preprocessor') {
|
||||
targetFlow.preprocessor_module = moduleToInsert
|
||||
return
|
||||
}
|
||||
|
||||
// Handle root level modules
|
||||
if (parentLocation.type === 'root') {
|
||||
const insertIndex = findBestInsertPosition(
|
||||
targetFlow.modules ?? [],
|
||||
sourceFlow.modules ?? [],
|
||||
parentLocation.index,
|
||||
moduleId
|
||||
)
|
||||
if (!targetFlow.modules) targetFlow.modules = []
|
||||
targetFlow.modules.splice(insertIndex, 0, moduleToInsert)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle nested modules
|
||||
insertIntoNestedParent(targetFlow, parentLocation, moduleToInsert, sourceFlow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module by ID anywhere in a flow (including nested modules, failure, and preprocessor)
|
||||
*
|
||||
* @param flow - The flow to search in
|
||||
* @param moduleId - The ID of the module to find
|
||||
* @returns The module if found, null otherwise
|
||||
*/
|
||||
export function findModuleInFlow(flow: FlowValue, moduleId: string): FlowModule | null {
|
||||
return findModuleById(flow, moduleId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the input schema has changed between two flow versions.
|
||||
* The input schema always exists (even if empty), so we only check for modifications.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,543 @@
|
||||
/**
|
||||
* Flow Diff Manager
|
||||
*
|
||||
* A reusable store for managing flow diff state, module actions, and accept/reject operations.
|
||||
* This decouples diff management from specific UI components (like AI chat) and makes it
|
||||
* available for any use case that needs to track and apply flow changes.
|
||||
*/
|
||||
|
||||
import type { ExtendedOpenFlow } from './types'
|
||||
import type { FlowModule, FlowValue } from '$lib/gen'
|
||||
import type { ModuleActionInfo } from './flowDiff'
|
||||
import {
|
||||
buildFlowTimeline,
|
||||
insertModuleIntoFlow,
|
||||
findModuleParent,
|
||||
locationsEqual,
|
||||
DUPLICATE_MODULE_PREFIX,
|
||||
NEW_MODULE_PREFIX
|
||||
} from './flowDiff'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import { getIndexInNestedModules } from '../copilot/chat/flow/utils'
|
||||
import { dfs } from './previousResults'
|
||||
import type DiffDrawer from '../DiffDrawer.svelte'
|
||||
import { SPECIAL_MODULE_IDS } from '../copilot/chat/shared'
|
||||
|
||||
export type FlowDiffManager = ReturnType<typeof createFlowDiffManager>
|
||||
|
||||
/**
|
||||
* Options for computing diff
|
||||
*/
|
||||
export type ComputeDiffOptions = {
|
||||
/** Mark all changes as pending (requiring user approval) */
|
||||
markAsPending?: boolean
|
||||
/** Mark removed modules as shadowed instead of removed (for visualization) */
|
||||
markRemovedAsShadowed?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a flow diff manager instance
|
||||
*/
|
||||
function createSkeletonModule(module: FlowModule): FlowModule {
|
||||
const clone = JSON.parse(JSON.stringify(module))
|
||||
if (clone.value.type === 'forloopflow' || clone.value.type === 'whileloopflow') {
|
||||
clone.value.modules = []
|
||||
} else if (clone.value.type === 'branchone') {
|
||||
clone.value.default = []
|
||||
clone.value.branches.forEach((b: any) => (b.modules = []))
|
||||
} else if (clone.value.type === 'branchall') {
|
||||
clone.value.branches.forEach((b: any) => (b.modules = []))
|
||||
} else if (clone.value.type === 'aiagent') {
|
||||
clone.value.tools = []
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
export function createFlowDiffManager({ testMode = false } = {}) {
|
||||
// State: snapshot of flow before changes
|
||||
let beforeFlow = $state<ExtendedOpenFlow | undefined>(undefined)
|
||||
|
||||
// State: current flow (after changes)
|
||||
let currentFlow = $state<FlowValue | undefined>(undefined)
|
||||
|
||||
// State: merged flow containing both original and modified/removed modules
|
||||
let mergedFlow = $state<FlowValue | undefined>(undefined)
|
||||
|
||||
// State: current input schema
|
||||
let currentInputSchema = $state<Record<string, any> | undefined>(undefined)
|
||||
|
||||
// State: whether to mark removed modules as shadowed (for side-by-side view)
|
||||
let markRemovedAsShadowed = $state(false)
|
||||
|
||||
// State: whether to allow accepting/rejecting changes to the flow
|
||||
let editMode = $state(false)
|
||||
|
||||
// State: module actions tracking changes (added/modified/removed/shadowed)
|
||||
let moduleActions = $state<Record<string, ModuleActionInfo>>({})
|
||||
|
||||
// Reference to DiffDrawer component for showing module diffs (not reactive)
|
||||
let diffDrawer: DiffDrawer | undefined = undefined
|
||||
|
||||
// Derived: whether there are any pending changes
|
||||
const hasPendingChanges = $derived(Object.values(moduleActions).some((info) => info.pending))
|
||||
|
||||
// Auto-compute diff when beforeFlow or currentFlow changes
|
||||
$effect(() => {
|
||||
if (beforeFlow && currentFlow) {
|
||||
const timeline = buildFlowTimeline(beforeFlow.value, currentFlow, {
|
||||
markRemovedAsShadowed: markRemovedAsShadowed,
|
||||
markAsPending: editMode
|
||||
})
|
||||
|
||||
// Store the merged flow for rendering
|
||||
mergedFlow = timeline.mergedFlow
|
||||
|
||||
// Update module actions
|
||||
const newActions = { ...timeline.afterActions }
|
||||
|
||||
// Check for input schema changes
|
||||
if (beforeFlow.schema && currentInputSchema) {
|
||||
const schemaChanged =
|
||||
JSON.stringify(beforeFlow.schema) !== JSON.stringify(currentInputSchema)
|
||||
if (schemaChanged) {
|
||||
newActions[SPECIAL_MODULE_IDS.INPUT] = {
|
||||
action: 'modified',
|
||||
pending: editMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateModuleActions(newActions)
|
||||
|
||||
// If no more actions, clear the snapshot (exit diff mode)
|
||||
if (Object.keys(newActions).length === 0 && !testMode) {
|
||||
clearSnapshot()
|
||||
}
|
||||
} else if (!beforeFlow) {
|
||||
// Clear module actions and merged flow when no snapshot
|
||||
mergedFlow = undefined
|
||||
updateModuleActions({})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper to update moduleActions and notify listeners
|
||||
*/
|
||||
function updateModuleActions(newActions: Record<string, ModuleActionInfo>) {
|
||||
moduleActions = newActions
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the before flow snapshot for diff computation
|
||||
*/
|
||||
function setBeforeFlow(flow: ExtendedOpenFlow | undefined) {
|
||||
beforeFlow = flow
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current flow state for diff computation
|
||||
*/
|
||||
function setCurrentFlow(flow: FlowValue | undefined) {
|
||||
currentFlow = flow
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current input schema for tracking schema changes
|
||||
*/
|
||||
function setCurrentInputSchema(schema: Record<string, any> | undefined) {
|
||||
currentInputSchema = schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to mark removed modules as shadowed (for side-by-side view)
|
||||
*/
|
||||
function setMarkRemovedAsShadowed(value: boolean) {
|
||||
markRemovedAsShadowed = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to edit the flow
|
||||
*/
|
||||
function setEditMode(value: boolean) {
|
||||
editMode = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the snapshot and all module actions
|
||||
*/
|
||||
function clearSnapshot() {
|
||||
beforeFlow = undefined
|
||||
currentFlow = undefined
|
||||
mergedFlow = undefined
|
||||
currentInputSchema = undefined
|
||||
updateModuleActions({})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set module actions directly (useful when actions are computed elsewhere)
|
||||
*/
|
||||
function setModuleActions(actions: Record<string, ModuleActionInfo>) {
|
||||
updateModuleActions(actions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get a module from a flow by ID
|
||||
*/
|
||||
function getModuleFromFlow(id: string, flow: ExtendedOpenFlow): FlowModule | undefined {
|
||||
if (flow.value.preprocessor_module?.id === id) {
|
||||
return flow.value.preprocessor_module
|
||||
} else if (flow.value.failure_module?.id === id) {
|
||||
return flow.value.failure_module
|
||||
} else {
|
||||
return dfs(id, flow, false)[0]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to delete a module from a flow object
|
||||
* Returns true if the module was found and deleted, false otherwise
|
||||
*/
|
||||
function deleteModuleInternal(id: string, flow: ExtendedOpenFlow): boolean {
|
||||
if (flow.value.preprocessor_module?.id === id) {
|
||||
flow.value.preprocessor_module = undefined
|
||||
return true
|
||||
} else if (flow.value.failure_module?.id === id) {
|
||||
flow.value.failure_module = undefined
|
||||
return true
|
||||
} else {
|
||||
const result = getIndexInNestedModules(flow, id)
|
||||
if (!result) {
|
||||
// Module not found (may have been deleted along with a parent)
|
||||
return false
|
||||
}
|
||||
const { modules } = result
|
||||
const index = modules.findIndex((m) => m.id === id)
|
||||
if (index >= 0) {
|
||||
modules.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to delete a module from the flow
|
||||
* Returns true if the module was found and deleted, false otherwise
|
||||
*/
|
||||
function deleteModuleFromFlow(
|
||||
id: string,
|
||||
flowStore: StateStore<ExtendedOpenFlow>,
|
||||
selectNextIdFn?: (id: string) => void
|
||||
): boolean {
|
||||
selectNextIdFn?.(id)
|
||||
const deleted = deleteModuleInternal(id, flowStore.val)
|
||||
if (deleted) {
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
return deleted
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a module action (keep the changes)
|
||||
* Removes the action from tracking after acceptance
|
||||
*/
|
||||
function acceptModule(id: string, flowStore?: StateStore<ExtendedOpenFlow>, asSkeleton = false) {
|
||||
if (!beforeFlow || !currentFlow) {
|
||||
console.warn('Cannot accept module without beforeFlow and currentFlow snapshots')
|
||||
return
|
||||
}
|
||||
|
||||
const info = moduleActions[id]
|
||||
if (!info) return
|
||||
|
||||
const actualId = id.startsWith(DUPLICATE_MODULE_PREFIX)
|
||||
? id.substring(DUPLICATE_MODULE_PREFIX.length)
|
||||
: id
|
||||
|
||||
if (id === SPECIAL_MODULE_IDS.INPUT) {
|
||||
// Accept input schema changes: update beforeFlow to match currentInputSchema
|
||||
if (beforeFlow.schema && currentInputSchema) {
|
||||
beforeFlow.schema = JSON.parse(JSON.stringify(currentInputSchema))
|
||||
}
|
||||
} else if (info.action === 'removed') {
|
||||
// Removed in after: Remove from beforeFlow
|
||||
deleteModuleInternal(actualId, beforeFlow)
|
||||
} else if (info.action === 'added') {
|
||||
// Added in after: Add to beforeFlow
|
||||
|
||||
// Check if parent exists in beforeFlow; if not, recursively accept parent first.
|
||||
const parentLoc = findModuleParent(currentFlow, actualId)
|
||||
if (
|
||||
parentLoc &&
|
||||
parentLoc.type !== 'root' &&
|
||||
parentLoc.type !== 'failure' &&
|
||||
parentLoc.type !== 'preprocessor'
|
||||
) {
|
||||
const parentInBefore = getModuleFromFlow(parentLoc.parentId, beforeFlow)
|
||||
if (!parentInBefore) {
|
||||
// Parent is missing in beforeFlow. It must be pending acceptance.
|
||||
// Accept as skeleton to avoid auto-accepting all siblings.
|
||||
acceptModule(parentLoc.parentId, flowStore, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Use insertModuleIntoFlow targeting beforeFlow, sourcing position from currentFlow
|
||||
let module = getModuleFromFlow(actualId, {
|
||||
value: currentFlow,
|
||||
summary: ''
|
||||
} as ExtendedOpenFlow)
|
||||
|
||||
if (module) {
|
||||
// Check if module already exists in beforeFlow (could be a skeleton from earlier acceptance)
|
||||
const existingModule = getModuleFromFlow(actualId, beforeFlow)
|
||||
|
||||
if (existingModule) {
|
||||
// Module exists in beforeFlow - check if it's in the same location
|
||||
const beforeLocation = findModuleParent(beforeFlow.value, actualId)
|
||||
const afterLocation = findModuleParent(currentFlow, actualId)
|
||||
|
||||
// Compare locations - if different, this is a move and we need to insert at new location
|
||||
const sameLocation = locationsEqual(beforeLocation, afterLocation)
|
||||
|
||||
if (sameLocation) {
|
||||
// Module is in the same location, update it in-place
|
||||
const moduleToApply = asSkeleton ? createSkeletonModule(module) : module
|
||||
Object.keys(existingModule).forEach((k) => delete (existingModule as any)[k])
|
||||
Object.assign(existingModule, $state.snapshot(moduleToApply))
|
||||
} else {
|
||||
// Module is being moved - insert at new location (the old copy will be removed when old__id is accepted)
|
||||
const moduleToInsert = asSkeleton ? createSkeletonModule(module) : module
|
||||
insertModuleIntoFlow(
|
||||
beforeFlow.value,
|
||||
$state.snapshot(moduleToInsert),
|
||||
currentFlow,
|
||||
actualId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Module doesn't exist, insert it
|
||||
const moduleToInsert = asSkeleton ? createSkeletonModule(module) : module
|
||||
insertModuleIntoFlow(
|
||||
beforeFlow.value,
|
||||
$state.snapshot(moduleToInsert),
|
||||
currentFlow,
|
||||
actualId
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (info.action === 'modified') {
|
||||
// Modified: Apply modifications to beforeFlow module
|
||||
const beforeModule = getModuleFromFlow(actualId, beforeFlow)
|
||||
const afterModule = getModuleFromFlow(actualId, {
|
||||
value: currentFlow,
|
||||
summary: ''
|
||||
} as ExtendedOpenFlow)
|
||||
|
||||
if (beforeModule && afterModule) {
|
||||
Object.keys(beforeModule).forEach((k) => delete (beforeModule as any)[k])
|
||||
Object.assign(beforeModule, $state.snapshot(afterModule))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a module action (revert the changes)
|
||||
* Removes the action from tracking after rejection
|
||||
*/
|
||||
function rejectModule(id: string, flowStore?: StateStore<ExtendedOpenFlow>) {
|
||||
if (!beforeFlow) {
|
||||
throw new Error('Cannot reject module without a beforeFlow snapshot')
|
||||
}
|
||||
|
||||
const actualId = id.startsWith(DUPLICATE_MODULE_PREFIX)
|
||||
? id.substring(DUPLICATE_MODULE_PREFIX.length)
|
||||
: id
|
||||
const info = moduleActions[id]
|
||||
|
||||
if (!info) return
|
||||
|
||||
// Only perform revert operations if flowStore is provided
|
||||
if (flowStore) {
|
||||
if (id === SPECIAL_MODULE_IDS.INPUT) {
|
||||
// Revert input schema changes
|
||||
flowStore.val.schema = beforeFlow.schema
|
||||
currentInputSchema = flowStore.val.schema
|
||||
} else if (info.action === 'added') {
|
||||
// Added in after: Remove from flowStore (currentFlow)
|
||||
// deleteModuleFromFlow handles the case where the module was already deleted (e.g., with its parent)
|
||||
deleteModuleFromFlow(actualId, flowStore)
|
||||
} else if (info.action === 'removed') {
|
||||
// Removed in after: Restore to flowStore (currentFlow)
|
||||
// Source from beforeFlow
|
||||
const oldModule = getModuleFromFlow(actualId, beforeFlow)
|
||||
if (oldModule) {
|
||||
// For type changes (old__ prefix), rename the new module to avoid ID conflict
|
||||
if (id.startsWith(DUPLICATE_MODULE_PREFIX)) {
|
||||
const existingNew = getModuleFromFlow(actualId, flowStore.val)
|
||||
if (existingNew) {
|
||||
existingNew.id = `${NEW_MODULE_PREFIX}${actualId}`
|
||||
}
|
||||
}
|
||||
insertModuleIntoFlow(
|
||||
flowStore.val.value,
|
||||
$state.snapshot(oldModule),
|
||||
beforeFlow.value,
|
||||
actualId
|
||||
)
|
||||
}
|
||||
refreshStateStore(flowStore)
|
||||
} else if (info.action === 'modified') {
|
||||
// Modified: Revert modifications in flowStore (currentFlow)
|
||||
const oldModule = getModuleFromFlow(actualId, beforeFlow)
|
||||
const newModule = getModuleFromFlow(actualId, flowStore.val)
|
||||
|
||||
if (oldModule && newModule) {
|
||||
Object.keys(newModule).forEach((k) => delete (newModule as any)[k])
|
||||
Object.assign(newModule, $state.snapshot(oldModule))
|
||||
}
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
|
||||
currentFlow = flowStore.val.value
|
||||
}
|
||||
|
||||
// Note: The $effect will automatically recompute the diff, clearing the action
|
||||
// since flowStore (currentFlow) now matches beforeFlow for this module.
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept all pending module actions
|
||||
*/
|
||||
function acceptAll(flowStore?: StateStore<ExtendedOpenFlow>) {
|
||||
const ids = Object.keys(moduleActions)
|
||||
for (const id of ids) {
|
||||
if (moduleActions[id]?.pending) {
|
||||
acceptModule(id, flowStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject all pending module actions (in reverse order for nested modules)
|
||||
*/
|
||||
function rejectAll(flowStore?: StateStore<ExtendedOpenFlow>) {
|
||||
const ids = Object.keys(moduleActions)
|
||||
// Process in reverse to handle nested modules correctly
|
||||
for (let i = ids.length - 1; i >= 0; i--) {
|
||||
if (moduleActions[ids[i]]?.pending) {
|
||||
rejectModule(ids[i], flowStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert the entire flow to the snapshot
|
||||
* @param flowStore - The flow store to update
|
||||
* @param snapshot - Optional specific snapshot to revert to (defaults to beforeFlow)
|
||||
*/
|
||||
function revertToSnapshot(flowStore: StateStore<ExtendedOpenFlow>, snapshot?: ExtendedOpenFlow) {
|
||||
const targetSnapshot = snapshot ?? beforeFlow
|
||||
if (!targetSnapshot) return
|
||||
|
||||
flowStore.val = targetSnapshot
|
||||
refreshStateStore(flowStore)
|
||||
clearSnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the DiffDrawer instance for showing module diffs
|
||||
*/
|
||||
function setDiffDrawer(drawer: DiffDrawer | undefined) {
|
||||
diffDrawer = drawer
|
||||
}
|
||||
|
||||
/**
|
||||
* Show diff for a specific module or Input schema
|
||||
*/
|
||||
function showModuleDiff(moduleId: string) {
|
||||
if (!diffDrawer || !beforeFlow) return
|
||||
|
||||
if (moduleId === SPECIAL_MODULE_IDS.INPUT) {
|
||||
// Show input schema diff
|
||||
diffDrawer.openDrawer()
|
||||
diffDrawer.setDiff({
|
||||
mode: 'simple',
|
||||
title: 'Flow Input Schema Diff',
|
||||
original: { schema: beforeFlow.schema ?? {} },
|
||||
current: { schema: currentInputSchema ?? {} }
|
||||
})
|
||||
} else {
|
||||
// Show module diff
|
||||
const beforeModule = getModuleFromFlow(moduleId, beforeFlow)
|
||||
// Need to check failure_module and preprocessor_module for currentFlow as well
|
||||
let afterModule: FlowModule | undefined = undefined
|
||||
if (currentFlow) {
|
||||
if (currentFlow.preprocessor_module?.id === moduleId) {
|
||||
afterModule = currentFlow.preprocessor_module
|
||||
} else if (currentFlow.failure_module?.id === moduleId) {
|
||||
afterModule = currentFlow.failure_module
|
||||
} else {
|
||||
afterModule = dfs(moduleId, { value: currentFlow, summary: '' }, false)[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (beforeModule && afterModule) {
|
||||
diffDrawer.openDrawer()
|
||||
diffDrawer.setDiff({
|
||||
mode: 'simple',
|
||||
title: `Module Diff: ${moduleId}`,
|
||||
original: beforeModule,
|
||||
current: afterModule
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State accessors
|
||||
get beforeFlow() {
|
||||
return beforeFlow
|
||||
},
|
||||
get currentFlow() {
|
||||
return currentFlow
|
||||
},
|
||||
get mergedFlow() {
|
||||
return mergedFlow
|
||||
},
|
||||
get moduleActions() {
|
||||
return moduleActions
|
||||
},
|
||||
get hasPendingChanges() {
|
||||
return hasPendingChanges
|
||||
},
|
||||
get currentInputSchema() {
|
||||
return currentInputSchema
|
||||
},
|
||||
get editModeEnabled() {
|
||||
return editMode
|
||||
},
|
||||
|
||||
// Snapshot management
|
||||
setBeforeFlow,
|
||||
setCurrentFlow,
|
||||
setCurrentInputSchema,
|
||||
setMarkRemovedAsShadowed,
|
||||
setEditMode,
|
||||
clearSnapshot,
|
||||
|
||||
// Module actions management
|
||||
setModuleActions,
|
||||
|
||||
// Accept/reject operations
|
||||
acceptModule,
|
||||
rejectModule,
|
||||
acceptAll,
|
||||
rejectAll,
|
||||
revertToSnapshot,
|
||||
|
||||
// Diff drawer management
|
||||
setDiffDrawer,
|
||||
showModuleDiff
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FlowModule, InputTransform, OpenFlow } from '$lib/gen'
|
||||
import { isFlowModuleTool } from './agentToolUtils'
|
||||
|
||||
type ModuleBranches = FlowModule[][]
|
||||
|
||||
@@ -9,6 +10,22 @@ export function getSubModules(flowModule: FlowModule): ModuleBranches {
|
||||
return flowModule.value.branches.map((branch) => branch.modules)
|
||||
} else if (flowModule.value.type == 'branchone') {
|
||||
return [...flowModule.value.branches.map((branch) => branch.modules), flowModule.value.default]
|
||||
} else if (flowModule.value.type === 'aiagent') {
|
||||
// Return AI agent tools as pseudo-FlowModules for searching
|
||||
if (flowModule.value.tools) {
|
||||
return [
|
||||
flowModule.value.tools
|
||||
.filter(isFlowModuleTool)
|
||||
.map(
|
||||
(tool) =>
|
||||
({
|
||||
id: tool.id,
|
||||
value: tool.value,
|
||||
summary: tool.summary
|
||||
}) as FlowModule
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -165,7 +165,15 @@ export async function createBranchAll(id: string): Promise<[FlowModule, FlowModu
|
||||
export async function createAiAgent(id: string): Promise<[FlowModule, FlowModuleState]> {
|
||||
const aiAgentFlowModules: FlowModule = {
|
||||
id,
|
||||
value: { type: 'aiagent', tools: [], input_transforms: {} }
|
||||
value: {
|
||||
type: 'aiagent',
|
||||
tools: [],
|
||||
input_transforms: {
|
||||
provider: { type: 'static', value: undefined },
|
||||
output_type: { type: 'static', value: 'text' },
|
||||
user_message: { type: 'static', value: undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flowModuleState = await loadFlowModuleState(aiAgentFlowModules)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { cleanFlow } from '../utils.svelte'
|
||||
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
|
||||
interface Props {
|
||||
drawer: Drawer | undefined
|
||||
@@ -15,7 +14,7 @@
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let flow = $derived(aiChatManager.flowAiChatHelpers?.getPreviewFlow() ?? flowStore.val)
|
||||
let flow = $derived(flowStore.val)
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { Play } from 'lucide-svelte'
|
||||
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
import type { GraphModuleState } from '$lib/components/graph'
|
||||
|
||||
interface Props {
|
||||
@@ -94,8 +93,7 @@
|
||||
'Input',
|
||||
'Trigger'
|
||||
].includes(upToSelected) ||
|
||||
upToSelected?.includes('branch') ||
|
||||
aiChatManager.flowAiChatHelpers?.getModuleAction(upToSelected) === 'removed'
|
||||
upToSelected?.includes('branch')
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { DiffIcon, Check, X } from 'lucide-svelte'
|
||||
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
||||
import type { FlowDiffManager } from '../flowDiffManager.svelte'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import type { OpenFlow } from '$lib/gen'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
interface Props {
|
||||
moduleId: string
|
||||
moduleAction: ModuleActionInfo | undefined
|
||||
diffManager: FlowDiffManager | undefined
|
||||
flowStore: StateStore<OpenFlow> | undefined
|
||||
placement?: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
let { moduleId, moduleAction, diffManager, flowStore, placement = 'top' }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if moduleAction && diffManager}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute right-0 left-0 flex flex-row z-20',
|
||||
placement === 'top' ? 'top-0 -translate-y-full' : 'bottom-0 translate-y-full',
|
||||
moduleAction.action === 'modified' ? 'justify-between' : 'justify-end'
|
||||
)}
|
||||
>
|
||||
{#if moduleAction?.action === 'modified' && diffManager.beforeFlow}
|
||||
<button
|
||||
class="p-1 bg-surface hover:bg-surface-hover rounded-t-md text-3xs font-normal flex flex-row items-center gap-1 text-orange-800 dark:text-orange-400"
|
||||
onclick={() => {
|
||||
diffManager?.showModuleDiff(moduleId)
|
||||
}}
|
||||
>
|
||||
<DiffIcon size={14} /> Diff
|
||||
</button>
|
||||
{/if}
|
||||
{#if moduleAction?.pending}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-row bg-surface overflow-hidden',
|
||||
placement === 'top' ? 'rounded-t-md' : 'rounded-b-md'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
class="p-1 bg-green-500 text-white hover:bg-green-600 text-3xs font-normal flex flex-row items-center gap-1"
|
||||
onclick={() => {
|
||||
if (flowStore) diffManager?.acceptModule(moduleId, flowStore)
|
||||
}}
|
||||
>
|
||||
<Check size={14} /> Accept
|
||||
</button>
|
||||
<button
|
||||
class="p-1 hover:bg-red-500 hover:text-white text-3xs font-normal flex flex-row items-center gap-1"
|
||||
onclick={() => {
|
||||
if (flowStore) diffManager?.rejectModule(moduleId, flowStore)
|
||||
}}
|
||||
>
|
||||
<X size={14} /> Reject
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import type { FlowDiffManager } from '../flowDiffManager.svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Bug, X } from 'lucide-svelte'
|
||||
import InsertModulePopover from '$lib/components/flows/map/InsertModulePopover.svelte'
|
||||
@@ -7,22 +8,18 @@
|
||||
import type { RawScript, ScriptLang } from '$lib/gen'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import ModuleAcceptReject, {
|
||||
getAiModuleAction
|
||||
} from '$lib/components/copilot/chat/flow/ModuleAcceptReject.svelte'
|
||||
import {
|
||||
aiModuleActionToBgColor,
|
||||
aiModuleActionToBorderColor,
|
||||
aiModuleActionToTextColor
|
||||
} from '$lib/components/copilot/chat/flow/utils'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import DiffActionBar from './DiffActionBar.svelte'
|
||||
import { getNodeColorClasses, aiActionToNodeState } from '$lib/components/graph'
|
||||
|
||||
let {
|
||||
disableAi,
|
||||
small
|
||||
small,
|
||||
diffManager
|
||||
}: {
|
||||
small: boolean
|
||||
disableAi?: boolean
|
||||
diffManager?: FlowDiffManager
|
||||
} = $props()
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -32,6 +29,14 @@
|
||||
const { selectionManager, flowStateStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const failureModuleId = $derived(flowStore.val?.value?.failure_module?.id)
|
||||
const moduleAction = $derived(
|
||||
failureModuleId ? diffManager?.moduleActions?.[failureModuleId] : undefined
|
||||
)
|
||||
const aiColorClasses = $derived(
|
||||
moduleAction ? getNodeColorClasses(aiActionToNodeState(moduleAction.action), false) : undefined
|
||||
)
|
||||
|
||||
async function insertFailureModule(
|
||||
inlineScript?: {
|
||||
language: RawScript['language']
|
||||
@@ -53,42 +58,43 @@
|
||||
selectionManager.selectId('failure')
|
||||
refreshStateStore(flowStore)
|
||||
}
|
||||
|
||||
const action = $derived(getAiModuleAction('failure'))
|
||||
</script>
|
||||
|
||||
{#if flowStore.val?.value?.failure_module}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
wrapperClasses={twMerge('min-w-36', small ? 'max-w-52' : 'max-w-64')}
|
||||
btnClasses={twMerge(
|
||||
aiModuleActionToBgColor(action),
|
||||
aiModuleActionToBorderColor(action),
|
||||
aiModuleActionToTextColor(action)
|
||||
)}
|
||||
id="flow-editor-error-handler"
|
||||
selected={selectionManager.getSelectedId()?.includes('failure')}
|
||||
onClick={() => {
|
||||
if (flowStore.val?.value?.failure_module) {
|
||||
selectionManager.selectId('failure')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModuleAcceptReject id="failure" {action} placement="bottom" />
|
||||
<div class="relative">
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
wrapperClasses={twMerge('min-w-36', small ? 'max-w-52' : 'max-w-64')}
|
||||
id="flow-editor-error-handler"
|
||||
selected={selectionManager.getSelectedId()?.includes('failure')}
|
||||
onClick={() => {
|
||||
if (flowStore.val?.value?.failure_module) {
|
||||
selectionManager.selectId('failure')
|
||||
}
|
||||
}}
|
||||
btnClasses={aiColorClasses?.bg ?? ''}
|
||||
>
|
||||
{#if failureModuleId}
|
||||
<DiffActionBar
|
||||
moduleId={failureModuleId}
|
||||
{moduleAction}
|
||||
{diffManager}
|
||||
{flowStore}
|
||||
placement="bottom"
|
||||
/>
|
||||
{/if}
|
||||
<Bug size={14} class="shrink-0" />
|
||||
|
||||
<Bug size={14} class="shrink-0" />
|
||||
<div class="truncate grow min-w-0 text-center text-xs">
|
||||
{flowStore.val.value.failure_module?.summary ||
|
||||
(flowStore.val.value.failure_module?.value.type === 'rawscript'
|
||||
? `${flowStore.val.value.failure_module?.value.language}`
|
||||
: 'TBD')}
|
||||
</div>
|
||||
|
||||
<div class="truncate grow min-w-0 text-center text-xs">
|
||||
{flowStore.val.value.failure_module?.summary ||
|
||||
(flowStore.val.value.failure_module?.value.type === 'rawscript'
|
||||
? `${flowStore.val.value.failure_module?.value.language}`
|
||||
: 'TBD')}
|
||||
</div>
|
||||
|
||||
{#if !action}
|
||||
<button
|
||||
title="Delete failure script"
|
||||
type="button"
|
||||
@@ -100,8 +106,8 @@
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
{/if}
|
||||
</Button>
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Index 0 is used by the tutorial to identify the first "Add step" -->
|
||||
<InsertModulePopover
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
Loader2,
|
||||
TriangleAlert,
|
||||
Timer,
|
||||
DiffIcon,
|
||||
Maximize2
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
@@ -37,22 +36,23 @@
|
||||
import OutputPicker from '$lib/components/flows/propPicker/OutputPicker.svelte'
|
||||
import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte'
|
||||
import type { FlowState } from '$lib/components/flows/flowState'
|
||||
import ModuleAcceptReject, {
|
||||
getAiModuleAction
|
||||
} from '$lib/components/copilot/chat/flow/ModuleAcceptReject.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import ModuleTest from '$lib/components/ModuleTest.svelte'
|
||||
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
|
||||
import { aiModuleActionToBgColor } from '$lib/components/copilot/chat/flow/utils'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { getNodeColorClasses, type FlowNodeState } from '$lib/components/graph'
|
||||
import type { AIModuleAction } from '$lib/components/copilot/chat/flow/core'
|
||||
import {
|
||||
getNodeColorClasses,
|
||||
aiActionToNodeState,
|
||||
type FlowNodeState
|
||||
} from '$lib/components/graph'
|
||||
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
||||
import DiffActionBar from './DiffActionBar.svelte'
|
||||
import { getGraphContext } from '$lib/components/graph/graphContext'
|
||||
|
||||
interface Props {
|
||||
selected?: boolean
|
||||
deletable?: boolean
|
||||
moduleAction: AIModuleAction | undefined
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
moduleAction: ModuleActionInfo | undefined
|
||||
retry?: boolean
|
||||
cache?: boolean
|
||||
earlyStop?: boolean
|
||||
@@ -95,7 +95,6 @@
|
||||
selected = false,
|
||||
deletable = false,
|
||||
moduleAction = undefined,
|
||||
onShowModuleDiff = undefined,
|
||||
retry = false,
|
||||
cache = false,
|
||||
earlyStop = false,
|
||||
@@ -127,12 +126,18 @@
|
||||
maximizeSubflow = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let colorClasses = $derived(getNodeColorClasses(nodeState, selected))
|
||||
|
||||
let pickableIds: Record<string, any> | undefined = $state(undefined)
|
||||
// Execution state takes priority over AI action colors
|
||||
let effectiveState = $derived(nodeState ?? aiActionToNodeState(moduleAction?.action))
|
||||
let colorClasses = $derived(getNodeColorClasses(effectiveState, selected))
|
||||
|
||||
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
|
||||
const flowInputsStore = flowEditorContext?.flowInputsStore
|
||||
const flowStore = flowEditorContext?.flowStore
|
||||
|
||||
const flowGraphContext = getGraphContext()
|
||||
const diffManager = flowGraphContext?.diffManager
|
||||
|
||||
let pickableIds: Record<string, any> | undefined = $state(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -186,8 +191,6 @@
|
||||
|
||||
const icon_render = $derived(icon)
|
||||
|
||||
const action = $derived(getAiModuleAction(id))
|
||||
|
||||
let testRunDropdownOpen = $state(false)
|
||||
|
||||
let outputPickerInner: OutputPickerInner | undefined = $state(undefined)
|
||||
@@ -252,9 +255,9 @@
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
{#if deletable && id && flowEditorContext?.flowStore && outputPickerVisible}
|
||||
{@const flowStore = flowEditorContext?.flowStore.val}
|
||||
{@const mod = flowStore?.value ? dfsPreviousResults(id, flowStore, false)[0] : undefined}
|
||||
{#if deletable && id && flowStore && outputPickerVisible}
|
||||
{@const flowStoreVal = flowStore.val}
|
||||
{@const mod = flowStoreVal?.value ? dfsPreviousResults(id, flowStoreVal, false)[0] : undefined}
|
||||
{#if mod && flowStateStore?.val?.[id]}
|
||||
<ModuleTest
|
||||
bind:this={moduleTest}
|
||||
@@ -269,13 +272,11 @@
|
||||
{/if}
|
||||
|
||||
<div class="relative">
|
||||
<!-- TODO: Use existing function to get module color classes instead of using aiModuleActionToBgColor -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full module flex rounded-md cursor-pointer max-w-full drop-shadow-base',
|
||||
deletable || moduleAction ? aiModuleActionToBgColor(moduleAction ?? action) : '',
|
||||
colorClasses.bg
|
||||
)}
|
||||
style="width: 275px; height: 34px;"
|
||||
@@ -283,21 +284,8 @@
|
||||
onmouseleave={() => (hover = false)}
|
||||
onpointerdown={stopPropagation(preventDefault((e) => dispatch('pointerdown', e)))}
|
||||
>
|
||||
{#if deletable}
|
||||
<ModuleAcceptReject action={moduleAction ?? action} {id} />
|
||||
{/if}
|
||||
{#if moduleAction === 'modified' && onShowModuleDiff && id}
|
||||
<div class="absolute right-0 left-0 top-0 -translate-y-full flex justify-start z-50">
|
||||
<Button
|
||||
class="p-1 bg-surface hover:bg-surface-hover rounded-t-md text-3xs font-normal flex flex-row items-center gap-1 text-orange-800 dark:text-orange-400"
|
||||
onClick={() => {
|
||||
onShowModuleDiff?.(id)
|
||||
}}
|
||||
startIcon={{ icon: DiffIcon }}
|
||||
>
|
||||
Diff
|
||||
</Button>
|
||||
</div>
|
||||
{#if id}
|
||||
<DiffActionBar moduleId={id} {moduleAction} {diffManager} {flowStore} />
|
||||
{/if}
|
||||
<div
|
||||
class={classNames('absolute z-0 rounded-md outline-offset-0', colorClasses.outline)}
|
||||
@@ -432,14 +420,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={twMerge('flex flex-col w-full', deletable && action === 'removed' ? 'opacity-50' : '')}
|
||||
>
|
||||
<div class="flex flex-col w-full">
|
||||
<FlowModuleSchemaItemViewer
|
||||
{label}
|
||||
{path}
|
||||
{id}
|
||||
deletable={deletable && !action}
|
||||
{deletable}
|
||||
{bold}
|
||||
bind:editId
|
||||
{hover}
|
||||
@@ -488,7 +474,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if deletable && !action}
|
||||
{#if deletable}
|
||||
{#if maximizeSubflow !== undefined}
|
||||
{@render buttonMaximizeSubflow?.()}
|
||||
{/if}
|
||||
|
||||
@@ -291,10 +291,15 @@
|
||||
|
||||
let graph: FlowGraphV2 | undefined = $state(undefined)
|
||||
let noteMode = $state(false)
|
||||
let diffManager = $derived(getDiffManager())
|
||||
export function isNodeVisible(nodeId: string): boolean {
|
||||
return graph?.isNodeVisible(nodeId) ?? false
|
||||
}
|
||||
|
||||
export function getDiffManager() {
|
||||
return graph?.getDiffManager()
|
||||
}
|
||||
|
||||
export function enableNotes(): void {
|
||||
graph?.enableNotes?.()
|
||||
}
|
||||
@@ -403,6 +408,7 @@
|
||||
{toggleAiChat}
|
||||
{noteMode}
|
||||
{toggleNoteMode}
|
||||
{diffManager}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -424,6 +430,8 @@
|
||||
{noteMode}
|
||||
notes={flowStore.val.value.notes}
|
||||
preprocessorModule={flowStore.val.value?.preprocessor_module}
|
||||
failureModule={flowStore.val.value?.failure_module}
|
||||
currentInputSchema={flowStore.val.schema}
|
||||
{selectionManager}
|
||||
{workspace}
|
||||
editMode
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import type { FlowDiffManager } from '../flowDiffManager.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { Badge } from '$lib/components/common'
|
||||
import { DollarSign, Settings, StickyNote } from 'lucide-svelte'
|
||||
@@ -18,6 +19,7 @@
|
||||
noteMode?: boolean
|
||||
toggleNoteMode?: () => void
|
||||
disableAi?: boolean
|
||||
diffManager?: FlowDiffManager
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -29,7 +31,8 @@
|
||||
toggleAiChat,
|
||||
noteMode,
|
||||
toggleNoteMode,
|
||||
disableAi
|
||||
disableAi,
|
||||
diffManager
|
||||
}: Props = $props()
|
||||
|
||||
const { selectionManager, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -54,7 +57,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
<Popover>
|
||||
<FlowErrorHandlerItem {disableAi} small={smallErrorHandler} on:generateStep />
|
||||
<FlowErrorHandlerItem {disableAi} small={smallErrorHandler} {diffManager} on:generateStep />
|
||||
{#snippet text()}
|
||||
Error Handler
|
||||
{/snippet}
|
||||
|
||||
@@ -15,15 +15,14 @@
|
||||
import type { FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { FlowNodeState } from '$lib/components/graph'
|
||||
import type { AIModuleAction } from '$lib/components/copilot/chat/flow/core'
|
||||
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
||||
import { getGraphContext } from '$lib/components/graph/graphContext'
|
||||
|
||||
interface Props {
|
||||
moduleId: string
|
||||
mod: FlowModule
|
||||
insertable: boolean
|
||||
moduleAction: AIModuleAction | undefined
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
moduleAction: ModuleActionInfo | undefined
|
||||
annotation?: string | undefined
|
||||
nodeState?: FlowNodeState
|
||||
moving?: string | undefined
|
||||
@@ -57,7 +56,6 @@
|
||||
mod = $bindable(),
|
||||
insertable,
|
||||
moduleAction = undefined,
|
||||
onShowModuleDiff = undefined,
|
||||
annotation = undefined,
|
||||
nodeState,
|
||||
moving = undefined,
|
||||
@@ -76,7 +74,8 @@
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
|
||||
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
|
||||
const { flowStore } = flowEditorContext || {}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
delete: CustomEvent<MouseEvent>
|
||||
@@ -122,7 +121,7 @@
|
||||
{#if duration_ms}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute z-10 right-0 -top-4 center-center text-primary text-2xs',
|
||||
'absolute z-5 right-0 -top-4 center-center text-primary text-2xs',
|
||||
editMode ? 'text-gray-400 dark:text-gray-500 text-2xs font-normal mr-2 right-16' : ''
|
||||
)}
|
||||
>
|
||||
@@ -159,7 +158,6 @@
|
||||
deletable={insertable}
|
||||
{editMode}
|
||||
{moduleAction}
|
||||
{onShowModuleDiff}
|
||||
label={`${
|
||||
mod.summary || (mod.value.type == 'forloopflow' ? 'For loop' : 'While loop')
|
||||
} ${mod.value.parallel ? '(parallel)' : ''} ${
|
||||
@@ -194,7 +192,6 @@
|
||||
deletable={insertable}
|
||||
{editMode}
|
||||
{moduleAction}
|
||||
{onShowModuleDiff}
|
||||
on:changeId
|
||||
on:delete
|
||||
on:move
|
||||
@@ -214,7 +211,6 @@
|
||||
deletable={insertable}
|
||||
{editMode}
|
||||
{moduleAction}
|
||||
{onShowModuleDiff}
|
||||
on:changeId
|
||||
on:delete
|
||||
on:move
|
||||
@@ -234,7 +230,6 @@
|
||||
{retries}
|
||||
{editMode}
|
||||
{moduleAction}
|
||||
{onShowModuleDiff}
|
||||
on:changeId
|
||||
on:pointerdown={handlePointerDown}
|
||||
on:delete
|
||||
|
||||
@@ -7,13 +7,9 @@
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Database, Square } from 'lucide-svelte'
|
||||
import ModuleAcceptReject, {
|
||||
getAiModuleAction
|
||||
} from '$lib/components/copilot/chat/flow/ModuleAcceptReject.svelte'
|
||||
import { aiModuleActionToBgColor } from '$lib/components/copilot/chat/flow/utils'
|
||||
import FlowGraphPreviewButton from './FlowGraphPreviewButton.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { getNodeColorClasses } from '$lib/components/graph'
|
||||
import { getNodeColorClasses, aiActionToNodeState } from '$lib/components/graph'
|
||||
|
||||
interface Props {
|
||||
label?: string | undefined
|
||||
@@ -58,7 +54,7 @@
|
||||
cache = false,
|
||||
earlyStop = false,
|
||||
editMode = false,
|
||||
action: actionProp = undefined,
|
||||
action = undefined,
|
||||
icon,
|
||||
onUpdateMock,
|
||||
onEditInput,
|
||||
@@ -77,7 +73,6 @@
|
||||
(nodeKind || (inputJson && Object.keys(inputJson).length > 0)) && editMode
|
||||
)
|
||||
|
||||
let action = $derived(actionProp ?? (label === 'Input' ? getAiModuleAction(label) : undefined))
|
||||
let hoverButton = $state(false)
|
||||
|
||||
const outputType = $derived(
|
||||
@@ -91,7 +86,9 @@
|
||||
: undefined
|
||||
: undefined
|
||||
)
|
||||
let colorClasses = $derived(getNodeColorClasses(outputType ?? '_VirtualItem', selected))
|
||||
// Execution state takes priority over AI action colors, fallback to _VirtualItem
|
||||
const effectiveState = $derived(outputType ?? aiActionToNodeState(action) ?? '_VirtualItem')
|
||||
let colorClasses = $derived(getNodeColorClasses(effectiveState, selected))
|
||||
</script>
|
||||
|
||||
<VirtualItemWrapper
|
||||
@@ -99,14 +96,10 @@
|
||||
{selectable}
|
||||
{id}
|
||||
outputPickerVisible={outputPickerVisible ?? false}
|
||||
className={editMode ? aiModuleActionToBgColor(action) : ''}
|
||||
{colorClasses}
|
||||
on:select
|
||||
>
|
||||
{#snippet children({ hover })}
|
||||
{#if editMode}
|
||||
<ModuleAcceptReject id="Input" {action} />
|
||||
{/if}
|
||||
<div class="flex flex-col w-full">
|
||||
<div
|
||||
class="flex flex-row justify-between {colorClasses.outline} {center
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type FlowModule, type FlowNote, type Job } from '../../gen'
|
||||
import { FlowService, type FlowModule, type FlowNote, type Job, type OpenFlow } from '../../gen'
|
||||
import { AI_OR_ASSET_NODE_TYPES, NODE, type GraphModuleState } from '.'
|
||||
import { getContext, onDestroy, onMount, tick, untrack, type Snippet } from 'svelte'
|
||||
import { createFlowDiffManager } from '../flows/flowDiffManager.svelte'
|
||||
|
||||
import { get, writable, type Writable } from 'svelte/store'
|
||||
import '@xyflow/svelte/dist/base.css'
|
||||
@@ -51,6 +52,7 @@
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import SubflowBound from './renderers/nodes/SubflowBound.svelte'
|
||||
import DiffDrawer from '../DiffDrawer.svelte'
|
||||
import ViewportResizer from './ViewportResizer.svelte'
|
||||
import ViewportSynchronizer from './ViewportSynchronizer.svelte'
|
||||
import AssetNode, { computeAssetNodes } from './renderers/nodes/AssetNode.svelte'
|
||||
@@ -69,7 +71,7 @@
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { AssetWithAltAccessType } from '../assets/lib'
|
||||
import type { AIModuleAction } from '../copilot/chat/flow/core'
|
||||
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
||||
import { setGraphContext } from './graphContext'
|
||||
import { computeNoteNodes } from './noteUtils.svelte'
|
||||
import { Tooltip } from '../meltComponents'
|
||||
@@ -81,6 +83,9 @@
|
||||
|
||||
const triggerContext = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
// Create diffManager instance for this FlowGraphV2
|
||||
const diffManager = createFlowDiffManager()
|
||||
|
||||
let fullWidth = 0
|
||||
let width = $state(0)
|
||||
|
||||
@@ -96,8 +101,7 @@
|
||||
notSelectable?: boolean
|
||||
flowModuleStates?: Record<string, GraphModuleState> | undefined
|
||||
testModuleStates?: ModulesTestStates
|
||||
moduleActions?: Record<string, AIModuleAction>
|
||||
inputSchemaModified?: boolean
|
||||
moduleActions?: Record<string, ModuleActionInfo>
|
||||
selectionManager?: SelectionManager
|
||||
path?: string | undefined
|
||||
newFlow?: boolean
|
||||
@@ -152,7 +156,6 @@
|
||||
onCancelTestFlow?: () => void
|
||||
onOpenPreview?: () => void
|
||||
onHideJobStatus?: () => void
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
flowHasChanged?: boolean
|
||||
exitNoteMode?: () => void
|
||||
onNotePositionUpdate?: (noteId: string, position: { x: number; y: number }) => void
|
||||
@@ -160,6 +163,10 @@
|
||||
sharedViewport?: Viewport
|
||||
onViewportChange?: (viewport: Viewport, isUserInitiated: boolean) => void
|
||||
leftHeader?: Snippet
|
||||
// Diff mode props
|
||||
diffBeforeFlow?: OpenFlow
|
||||
currentInputSchema?: Record<string, any>
|
||||
markRemovedAsShadowed?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -182,7 +189,6 @@
|
||||
flowModuleStates = undefined,
|
||||
testModuleStates = undefined,
|
||||
moduleActions = undefined,
|
||||
inputSchemaModified = undefined,
|
||||
selectionManager: selectionManagerProp = undefined,
|
||||
path = undefined,
|
||||
newFlow = false,
|
||||
@@ -207,7 +213,6 @@
|
||||
onCancelTestFlow = undefined,
|
||||
onOpenPreview = undefined,
|
||||
onHideJobStatus = undefined,
|
||||
onShowModuleDiff = undefined,
|
||||
individualStepTests = false,
|
||||
flowJob = undefined,
|
||||
showJobStatus = false,
|
||||
@@ -221,6 +226,9 @@
|
||||
sharedViewport = undefined,
|
||||
onViewportChange = undefined,
|
||||
leftHeader = undefined,
|
||||
diffBeforeFlow = undefined,
|
||||
currentInputSchema = undefined,
|
||||
markRemovedAsShadowed = false,
|
||||
multiSelectEnabled = false
|
||||
}: Props = $props()
|
||||
|
||||
@@ -273,7 +281,8 @@
|
||||
showAssets,
|
||||
noteManager,
|
||||
clearFlowSelection,
|
||||
yOffset
|
||||
yOffset,
|
||||
diffManager
|
||||
} as any)
|
||||
|
||||
if (triggerContext && allowSimplifiedPoll) {
|
||||
@@ -294,6 +303,7 @@
|
||||
if (isSimplifiable(modules)) {
|
||||
triggerContext?.simplifiedPoll?.set(undefined)
|
||||
}
|
||||
diffManager.setDiffDrawer(undefined)
|
||||
})
|
||||
|
||||
function onModulesChange(modules: FlowModule[]) {
|
||||
@@ -454,7 +464,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
let moduleTracker = new ChangeTracker($state.snapshot(modules))
|
||||
// Validation: error if both diffBeforeFlow and moduleActions are provided
|
||||
$effect(() => {
|
||||
if (diffBeforeFlow && moduleActions) {
|
||||
throw new Error('Cannot provide both diffBeforeFlow and moduleActions props to FlowGraphV2')
|
||||
}
|
||||
})
|
||||
|
||||
// Sync props to diffManager
|
||||
$effect(() => {
|
||||
const currentFlowValue = {
|
||||
modules: modules,
|
||||
failure_module: failureModule,
|
||||
preprocessor_module: preprocessorModule
|
||||
}
|
||||
diffManager.setCurrentFlow(currentFlowValue)
|
||||
diffManager.setCurrentInputSchema(currentInputSchema)
|
||||
|
||||
// Handle diff mode setup
|
||||
if (diffBeforeFlow) {
|
||||
diffManager.setEditMode(editMode)
|
||||
diffManager.setBeforeFlow(diffBeforeFlow)
|
||||
diffManager.setMarkRemovedAsShadowed(markRemovedAsShadowed)
|
||||
} else if (moduleActions) {
|
||||
// Display-only mode: just set the module actions
|
||||
diffManager.setModuleActions(moduleActions)
|
||||
}
|
||||
})
|
||||
|
||||
// Use diffManager state for rendering
|
||||
let effectiveModuleActions = $derived(diffManager.moduleActions)
|
||||
|
||||
// Use merged flow when in diff mode (includes removed modules), otherwise use raw modules
|
||||
let effectiveModules = $derived(diffManager.mergedFlow?.modules ?? modules)
|
||||
|
||||
let effectiveFailureModule = $derived(diffManager.mergedFlow?.failure_module ?? failureModule)
|
||||
|
||||
let effectivePreprocessorModule = $derived(
|
||||
diffManager.mergedFlow?.preprocessor_module ?? preprocessorModule
|
||||
)
|
||||
|
||||
let canUseDiffDrawer = $derived(diffBeforeFlow || moduleActions || editMode)
|
||||
|
||||
// Initialize moduleTracker with effectiveModules
|
||||
let moduleTracker = $state(new ChangeTracker<FlowModule[]>([]))
|
||||
|
||||
let nodes = $state.raw<Node[]>([])
|
||||
let edges = $state.raw<Edge[]>([])
|
||||
@@ -641,6 +694,7 @@
|
||||
// centerViewport(width)
|
||||
// })
|
||||
let yamlEditorDrawer: Drawer | undefined = $state(undefined)
|
||||
let diffDrawer: DiffDrawer | undefined = $state(undefined)
|
||||
|
||||
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
|
||||
|
||||
@@ -648,21 +702,26 @@
|
||||
allowSimplifiedPoll && modules && untrack(() => onModulesChange(modules ?? []))
|
||||
})
|
||||
$effect(() => {
|
||||
readFieldsRecursively(modules)
|
||||
untrack(() => moduleTracker.track($state.snapshot(modules)))
|
||||
readFieldsRecursively(effectiveModules)
|
||||
untrack(() => moduleTracker.track($state.snapshot(effectiveModules)))
|
||||
})
|
||||
|
||||
// Wire up the diff drawer to the diffManager
|
||||
$effect(() => {
|
||||
diffManager.setDiffDrawer(diffDrawer)
|
||||
})
|
||||
|
||||
let graph = $derived.by(() => {
|
||||
moduleTracker.counter
|
||||
effectiveModuleActions
|
||||
return graphBuilder(
|
||||
untrack(() => modules),
|
||||
untrack(() => effectiveModules),
|
||||
{
|
||||
disableAi,
|
||||
insertable,
|
||||
flowModuleStates: untrack(() => flowModuleStates),
|
||||
testModuleStates: untrack(() => testModuleStates),
|
||||
moduleActions: untrack(() => moduleActions),
|
||||
inputSchemaModified: untrack(() => inputSchemaModified),
|
||||
moduleActions: untrack(() => effectiveModuleActions),
|
||||
selectedId: untrack(() => selectedId),
|
||||
path,
|
||||
newFlow,
|
||||
@@ -677,11 +736,10 @@
|
||||
suspendStatus,
|
||||
flowHasChanged,
|
||||
chatInputEnabled,
|
||||
onShowModuleDiff: untrack(() => onShowModuleDiff),
|
||||
additionalAssetsMap: flowGraphAssetsCtx?.val.additionalAssetsMap
|
||||
},
|
||||
untrack(() => failureModule),
|
||||
preprocessorModule,
|
||||
untrack(() => effectiveFailureModule),
|
||||
effectivePreprocessorModule,
|
||||
eventHandler,
|
||||
success,
|
||||
$useDataflow,
|
||||
@@ -805,6 +863,10 @@
|
||||
viewportSynchronizer?.zoomOut()
|
||||
}
|
||||
|
||||
export function getDiffManager() {
|
||||
return diffManager
|
||||
}
|
||||
|
||||
export function enableNotes() {
|
||||
if (!showNotes) {
|
||||
showNotes = true
|
||||
@@ -817,6 +879,9 @@
|
||||
{#if insertable}
|
||||
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
|
||||
{/if}
|
||||
{#if canUseDiffDrawer}
|
||||
<DiffDrawer bind:this={diffDrawer} />
|
||||
{/if}
|
||||
<div
|
||||
style={`height: ${height}px; max-height: ${maxHeight}px;`}
|
||||
class="overflow-clip relative"
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { GraphModuleState } from './model'
|
||||
import { getFlowModuleAssets, type AssetWithAltAccessType } from '../assets/lib'
|
||||
import { assetDisplaysAsOutputInFlowGraph } from './renderers/nodes/AssetNode.svelte'
|
||||
import type { ModulesTestStates, ModuleTestState } from '../modulesTest.svelte'
|
||||
import { type AIModuleAction } from '../copilot/chat/flow/core'
|
||||
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
|
||||
|
||||
export type InsertKind =
|
||||
| 'script'
|
||||
@@ -128,8 +128,7 @@ export type InputN = {
|
||||
showJobStatus: boolean
|
||||
flowHasChanged: boolean
|
||||
chatInputEnabled: boolean
|
||||
inputSchemaModified?: boolean
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
moduleAction?: ModuleActionInfo
|
||||
assets?: AssetWithAltAccessType[] | undefined
|
||||
}
|
||||
}
|
||||
@@ -150,8 +149,7 @@ export type ModuleN = {
|
||||
flowJob: Job | undefined
|
||||
isOwner: boolean
|
||||
assets: AssetWithAltAccessType[] | undefined
|
||||
moduleAction: AIModuleAction | undefined
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
moduleAction: ModuleActionInfo | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,8 +368,7 @@ export function graphBuilder(
|
||||
insertable: boolean
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
testModuleStates: ModulesTestStates | undefined
|
||||
moduleActions?: Record<string, AIModuleAction>
|
||||
inputSchemaModified?: boolean
|
||||
moduleActions?: Record<string, ModuleActionInfo>
|
||||
selectedId: string | undefined
|
||||
path: string | undefined
|
||||
newFlow: boolean
|
||||
@@ -386,7 +383,6 @@ export function graphBuilder(
|
||||
suspendStatus: Record<string, { job: Job; nb: number }>
|
||||
flowHasChanged: boolean
|
||||
chatInputEnabled: boolean
|
||||
onShowModuleDiff?: (moduleId: string) => void
|
||||
additionalAssetsMap?: Record<string, AssetWithAltAccessType[]>
|
||||
},
|
||||
failureModule: FlowModule | undefined,
|
||||
@@ -445,8 +441,7 @@ export function graphBuilder(
|
||||
isOwner: extra.isOwner,
|
||||
flowJob: extra.flowJob,
|
||||
assets: getFlowModuleAssets(module, extra.additionalAssetsMap),
|
||||
moduleAction: extra.moduleActions?.[module.id],
|
||||
onShowModuleDiff: extra.onShowModuleDiff
|
||||
moduleAction: extra.moduleActions?.[module.id]
|
||||
},
|
||||
type: 'module',
|
||||
selectable: true
|
||||
@@ -566,8 +561,7 @@ export function graphBuilder(
|
||||
showJobStatus: extra.showJobStatus,
|
||||
flowHasChanged: extra.flowHasChanged,
|
||||
chatInputEnabled: extra.chatInputEnabled,
|
||||
inputSchemaModified: extra.inputSchemaModified,
|
||||
onShowModuleDiff: extra.onShowModuleDiff,
|
||||
moduleAction: extra.moduleActions?.['Input'],
|
||||
...(inputAssets ? { assets: inputAssets } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getContext, setContext } from 'svelte'
|
||||
import type { SelectionManager } from './selectionUtils.svelte'
|
||||
import type { NoteManager } from './noteManager.svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { FlowDiffManager } from '../flows/flowDiffManager.svelte'
|
||||
|
||||
export type GraphContext = {
|
||||
selectionManager: SelectionManager
|
||||
@@ -10,6 +11,7 @@ export type GraphContext = {
|
||||
noteManager?: NoteManager
|
||||
clearFlowSelection?: () => void
|
||||
yOffset?: number
|
||||
diffManager: FlowDiffManager
|
||||
}
|
||||
|
||||
const graphContextKey = 'FlowGraphContext'
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
import InsertModulePopover from '$lib/components/flows/map/InsertModulePopover.svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import DiffActionBar from '$lib/components/flows/map/DiffActionBar.svelte'
|
||||
import { schemaToObject } from '$lib/schema'
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { MessageSquare, DiffIcon } from 'lucide-svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { MessageSquare } from 'lucide-svelte'
|
||||
import { getGraphContext } from '../../graphContext'
|
||||
import FunnelCog from '$lib/components/icons/FunnelCog.svelte'
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectionManager } = getGraphContext()
|
||||
const { selectionManager, diffManager } = getGraphContext()
|
||||
|
||||
const { previewArgs, flowStore } =
|
||||
getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
|
||||
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
|
||||
const { previewArgs, flowStore } = flowEditorContext || {}
|
||||
|
||||
let topFlowInput = $derived(
|
||||
flowStore?.val && previewArgs && flowStore?.val?.schema
|
||||
@@ -34,17 +34,12 @@
|
||||
let inputLabel = $derived(data.chatInputEnabled ? 'Chat message' : 'Input')
|
||||
</script>
|
||||
|
||||
{#if data.inputSchemaModified && data.onShowModuleDiff}
|
||||
<div class="absolute right-0 left-0 top-0 -translate-y-full flex justify-start z-50">
|
||||
<Button
|
||||
class="p-1 bg-surface hover:bg-surface-hover rounded-t-md text-3xs font-normal flex flex-row items-center gap-1 text-orange-800 dark:text-orange-400"
|
||||
onClick={() => {
|
||||
data.onShowModuleDiff?.('Input')
|
||||
}}
|
||||
startIcon={{ icon: DiffIcon }}>Diff</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<DiffActionBar
|
||||
moduleId="Input"
|
||||
moduleAction={data.moduleAction}
|
||||
{diffManager}
|
||||
flowStore={flowEditorContext?.flowStore}
|
||||
/>
|
||||
|
||||
<NodeWrapper>
|
||||
{#snippet children({ darkMode })}
|
||||
@@ -99,7 +94,7 @@
|
||||
cache={data.cache}
|
||||
earlyStop={data.earlyStop}
|
||||
editMode={data.editMode}
|
||||
action={data.inputSchemaModified ? 'modified' : undefined}
|
||||
action={data.moduleAction?.action}
|
||||
onEditInput={data.eventHandlers.editInput}
|
||||
onTestFlow={() => {
|
||||
data.eventHandlers.testFlow()
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
insertable={data.insertable}
|
||||
editMode={data.editMode}
|
||||
moduleAction={data.moduleAction}
|
||||
onShowModuleDiff={data.onShowModuleDiff}
|
||||
annotation={flowJobs &&
|
||||
(data.module.value.type === 'forloopflow' || data.module.value.type === 'whileloopflow')
|
||||
? 'Iteration: ' +
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FlowStatusModule } from '$lib/gen'
|
||||
import type { AIModuleAction } from '$lib/components/flows/flowDiff'
|
||||
|
||||
export const NODE = {
|
||||
width: 275,
|
||||
@@ -18,7 +19,33 @@ export type FlowNodeColorClasses = {
|
||||
|
||||
export const AI_OR_ASSET_NODE_TYPES = ['asset', 'assetsOverflowed', 'newAiTool', 'aiTool']
|
||||
|
||||
export type FlowNodeState = FlowStatusModule['type'] | '_VirtualItem' | '_Skipped' | undefined
|
||||
export type FlowNodeState =
|
||||
| FlowStatusModule['type']
|
||||
| '_VirtualItem'
|
||||
| '_Skipped'
|
||||
| '_AIAdded'
|
||||
| '_AIModified'
|
||||
| '_AIRemoved'
|
||||
| '_AIShadowed'
|
||||
| undefined
|
||||
|
||||
/**
|
||||
* Convert AI module action to FlowNodeState
|
||||
*/
|
||||
export function aiActionToNodeState(action: AIModuleAction): FlowNodeState {
|
||||
switch (action) {
|
||||
case 'added':
|
||||
return '_AIAdded'
|
||||
case 'modified':
|
||||
return '_AIModified'
|
||||
case 'removed':
|
||||
return '_AIRemoved'
|
||||
case 'shadowed':
|
||||
return '_AIShadowed'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeColorClasses(state: FlowNodeState, selected: boolean): FlowNodeColorClasses {
|
||||
let outlined = ' outline outline-1 active:outline active:outline-1'
|
||||
@@ -113,6 +140,63 @@ export function getNodeColorClasses(state: FlowNodeState, selected: boolean): Fl
|
||||
badge: 'bg-purple-200 text-purple-700'
|
||||
}
|
||||
},
|
||||
// AI Module Action states (distinct shades from execution states)
|
||||
_AIAdded: {
|
||||
selected: {
|
||||
bg: 'bg-green-300 dark:bg-green-800',
|
||||
outline: 'outline-green-600 dark:outline-green-500' + outlined,
|
||||
text: 'text-green-900 dark:text-green-100',
|
||||
badge: 'bg-green-200 text-green-800'
|
||||
},
|
||||
notSelected: {
|
||||
bg: 'bg-green-300 dark:bg-green-900',
|
||||
outline: '',
|
||||
text: 'text-green-800 dark:text-green-200',
|
||||
badge: 'bg-green-300 text-green-800'
|
||||
}
|
||||
},
|
||||
_AIModified: {
|
||||
selected: {
|
||||
bg: 'bg-orange-300 dark:bg-orange-800',
|
||||
outline: 'outline-orange-600' + outlined,
|
||||
text: 'text-orange-900 dark:text-orange-100',
|
||||
badge: 'bg-orange-200 text-orange-800'
|
||||
},
|
||||
notSelected: {
|
||||
bg: 'bg-orange-300 dark:bg-orange-900',
|
||||
outline: '',
|
||||
text: 'text-orange-800 dark:text-orange-200',
|
||||
badge: 'bg-orange-300 text-orange-800'
|
||||
}
|
||||
},
|
||||
_AIRemoved: {
|
||||
selected: {
|
||||
bg: 'bg-red-300/50 dark:bg-red-800/50',
|
||||
outline: 'outline-red-600' + outlined,
|
||||
text: 'text-red-900 dark:text-red-100',
|
||||
badge: 'bg-red-200 text-red-800'
|
||||
},
|
||||
notSelected: {
|
||||
bg: 'bg-red-300/50 dark:bg-red-900/50',
|
||||
outline: '',
|
||||
text: 'text-red-800 dark:text-red-200',
|
||||
badge: 'bg-red-300 text-red-800'
|
||||
}
|
||||
},
|
||||
_AIShadowed: {
|
||||
selected: {
|
||||
bg: 'bg-gray-300/30 dark:bg-gray-600/30 opacity-50',
|
||||
outline: 'outline-gray-500' + outlined,
|
||||
text: 'text-gray-700 dark:text-gray-300',
|
||||
badge: 'bg-gray-200 text-gray-700'
|
||||
},
|
||||
notSelected: {
|
||||
bg: 'bg-gray-300/30 dark:bg-gray-700/30 opacity-50',
|
||||
outline: '',
|
||||
text: 'text-gray-600 dark:text-gray-400',
|
||||
badge: 'bg-gray-300 text-gray-700'
|
||||
}
|
||||
},
|
||||
default: defaultStyle
|
||||
} as Record<
|
||||
NonNullable<FlowNodeState> | 'default',
|
||||
|
||||
+34
-13
@@ -1,3 +1,4 @@
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import { readFileSync } from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -32,9 +33,7 @@ const config = {
|
||||
'public.windmill.xyz'
|
||||
],
|
||||
port: 3000,
|
||||
cors: {
|
||||
origin: '*'
|
||||
},
|
||||
cors: { origin: '*' },
|
||||
proxy: {
|
||||
'^/api/w/[^/]+/s3_proxy/.*': {
|
||||
target: process.env.REMOTE ?? 'https://app.windmill.dev/',
|
||||
@@ -44,6 +43,7 @@ const config = {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
// Prevent collapsing slashes during URL normalization
|
||||
const originalPath = req.url
|
||||
|
||||
proxyReq.path = originalPath
|
||||
})
|
||||
}
|
||||
@@ -74,13 +74,9 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
port: 3001
|
||||
},
|
||||
preview: { port: 3001 },
|
||||
plugins: [sveltekit(), ...(process.env.HTTPS === 'true' ? [mkcert()] : []), plugin],
|
||||
define: {
|
||||
__pkg__: version
|
||||
},
|
||||
define: { __pkg__: version },
|
||||
optimizeDeps: {
|
||||
include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim', 'monaco-editor-wrapper'],
|
||||
exclude: [
|
||||
@@ -89,9 +85,7 @@ const config = {
|
||||
'vscode'
|
||||
]
|
||||
},
|
||||
worker: {
|
||||
format: 'es'
|
||||
},
|
||||
worker: { format: 'es' },
|
||||
resolve: {
|
||||
alias: {
|
||||
path: 'path-browserify',
|
||||
@@ -100,7 +94,34 @@ const config = {
|
||||
},
|
||||
dedupe: ['vscode', 'monaco-editor']
|
||||
},
|
||||
assetsInclude: ['**/*.wasm']
|
||||
assetsInclude: ['**/*.wasm'],
|
||||
test: {
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
{
|
||||
extends: './vite.config.js',
|
||||
test: {
|
||||
name: 'client',
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium', headless: true }]
|
||||
},
|
||||
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/lib/server/**']
|
||||
}
|
||||
},
|
||||
{
|
||||
extends: './vite.config.js',
|
||||
test: {
|
||||
name: 'server',
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
+157
-5
@@ -22,56 +22,75 @@ components:
|
||||
schemas:
|
||||
OpenFlow:
|
||||
type: object
|
||||
description: Top-level flow definition containing metadata, configuration, and the flow structure
|
||||
properties:
|
||||
summary:
|
||||
type: string
|
||||
description: Short description of what this flow does
|
||||
description:
|
||||
type: string
|
||||
description: Detailed documentation for this flow
|
||||
value:
|
||||
$ref: "#/components/schemas/FlowValue"
|
||||
schema:
|
||||
type: object
|
||||
description: JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-<type>' (e.g., 'resource-stripe')
|
||||
required:
|
||||
- summary
|
||||
- value
|
||||
|
||||
FlowValue:
|
||||
type: object
|
||||
description: The flow structure containing modules and optional preprocessor/failure handlers
|
||||
properties:
|
||||
modules:
|
||||
type: array
|
||||
description: Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch
|
||||
items:
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
failure_module:
|
||||
description: Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
preprocessor_module:
|
||||
description: Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
same_worker:
|
||||
type: boolean
|
||||
description: If true, all steps run on the same worker for better performance
|
||||
concurrent_limit:
|
||||
type: number
|
||||
description: Maximum number of concurrent executions of this flow
|
||||
concurrency_key:
|
||||
type: string
|
||||
description: Expression to group concurrent executions (e.g., by user ID)
|
||||
concurrency_time_window_s:
|
||||
type: number
|
||||
description: Time window in seconds for concurrent_limit
|
||||
debounce_delay_s:
|
||||
type: number
|
||||
description: Delay in seconds to debounce flow executions
|
||||
debounce_key:
|
||||
type: string
|
||||
description: Expression to group debounced executions
|
||||
skip_expr:
|
||||
type: string
|
||||
description: JavaScript expression to conditionally skip the entire flow
|
||||
cache_ttl:
|
||||
type: number
|
||||
description: Cache duration in seconds for flow results
|
||||
cache_ignore_s3_path:
|
||||
type: boolean
|
||||
flow_env:
|
||||
type: object
|
||||
description: Environment variables available to all steps
|
||||
additionalProperties:
|
||||
type: string
|
||||
priority:
|
||||
type: number
|
||||
description: Execution priority (higher numbers run first)
|
||||
early_return:
|
||||
type: string
|
||||
description: JavaScript expression to return early from the flow
|
||||
chat_input_enabled:
|
||||
type: boolean
|
||||
description: Whether this flow accepts chat-style input
|
||||
@@ -85,28 +104,37 @@ components:
|
||||
|
||||
Retry:
|
||||
type: object
|
||||
description: Retry configuration for failed module executions
|
||||
properties:
|
||||
constant:
|
||||
type: object
|
||||
description: Retry with constant delay between attempts
|
||||
properties:
|
||||
attempts:
|
||||
type: integer
|
||||
description: Number of retry attempts
|
||||
seconds:
|
||||
type: integer
|
||||
description: Seconds to wait between retries
|
||||
exponential:
|
||||
type: object
|
||||
description: Retry with exponential backoff (delay doubles each time)
|
||||
properties:
|
||||
attempts:
|
||||
type: integer
|
||||
description: Number of retry attempts
|
||||
multiplier:
|
||||
type: integer
|
||||
description: Multiplier for exponential backoff
|
||||
seconds:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Initial delay in seconds
|
||||
random_factor:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Random jitter percentage (0-100) to avoid thundering herd
|
||||
retry_if:
|
||||
$ref: "#/components/schemas/RetryIf"
|
||||
|
||||
@@ -170,93 +198,127 @@ components:
|
||||
|
||||
RetryIf:
|
||||
type: object
|
||||
description: Conditional retry based on error or result
|
||||
properties:
|
||||
expr:
|
||||
type: string
|
||||
description: JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables
|
||||
required:
|
||||
- expr
|
||||
|
||||
StopAfterIf:
|
||||
type: object
|
||||
description: Early termination condition for a module
|
||||
properties:
|
||||
skip_if_stopped:
|
||||
type: boolean
|
||||
description: If true, following steps are skipped when this condition triggers
|
||||
expr:
|
||||
type: string
|
||||
description: JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop
|
||||
error_message:
|
||||
type: string
|
||||
description: Custom error message shown when stopping
|
||||
required:
|
||||
- expr
|
||||
|
||||
FlowModule:
|
||||
type: object
|
||||
description: A single step in a flow. Can be a script, subflow, loop, or branch
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)
|
||||
value:
|
||||
$ref: "#/components/schemas/FlowModuleValue"
|
||||
stop_after_if:
|
||||
description: Early termination condition evaluated after this step completes
|
||||
$ref: "#/components/schemas/StopAfterIf"
|
||||
stop_after_all_iters_if:
|
||||
description: For loops only - early termination condition evaluated after all iterations complete
|
||||
$ref: "#/components/schemas/StopAfterIf"
|
||||
skip_if:
|
||||
type: object
|
||||
description: Conditionally skip this step based on previous results or flow inputs
|
||||
properties:
|
||||
expr:
|
||||
type: string
|
||||
description: JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.<step_id>'
|
||||
required:
|
||||
- expr
|
||||
sleep:
|
||||
description: Delay before executing this step (in seconds or as expression)
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
cache_ttl:
|
||||
type: number
|
||||
description: Cache duration in seconds for this step's results
|
||||
cache_ignore_s3_path:
|
||||
type: boolean
|
||||
timeout:
|
||||
description: Maximum execution time in seconds (static value or expression)
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
delete_after_use:
|
||||
type: boolean
|
||||
description: If true, this step's result is deleted after use to save memory
|
||||
summary:
|
||||
type: string
|
||||
description: Short description of what this step does
|
||||
mock:
|
||||
type: object
|
||||
description: Mock configuration for testing without executing the actual step
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
return_value: {}
|
||||
description: If true, return mock value instead of executing
|
||||
return_value:
|
||||
description: Value to return when mocked
|
||||
suspend:
|
||||
type: object
|
||||
description: Configuration for approval/resume steps that wait for user input
|
||||
properties:
|
||||
required_events:
|
||||
type: integer
|
||||
description: Number of approvals required before continuing
|
||||
timeout:
|
||||
type: integer
|
||||
description: Timeout in seconds before auto-continuing or canceling
|
||||
resume_form:
|
||||
type: object
|
||||
description: Form schema for collecting input when resuming
|
||||
properties:
|
||||
schema:
|
||||
type: object
|
||||
description: JSON Schema for the resume form
|
||||
user_auth_required:
|
||||
type: boolean
|
||||
description: If true, only authenticated users can approve
|
||||
user_groups_required:
|
||||
description: Expression or list of groups that can approve
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
self_approval_disabled:
|
||||
type: boolean
|
||||
description: If true, the user who started the flow cannot approve
|
||||
hide_cancel:
|
||||
type: boolean
|
||||
description: If true, hide the cancel button on the approval form
|
||||
continue_on_disapprove_timeout:
|
||||
type: boolean
|
||||
description: If true, continue flow on timeout instead of canceling
|
||||
priority:
|
||||
type: number
|
||||
description: Execution priority for this step (higher numbers run first)
|
||||
continue_on_error:
|
||||
type: boolean
|
||||
description: If true, flow continues even if this step fails
|
||||
retry:
|
||||
description: Retry configuration if this step fails
|
||||
$ref: "#/components/schemas/Retry"
|
||||
required:
|
||||
- value
|
||||
- id
|
||||
|
||||
InputTransform:
|
||||
description: Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/StaticTransform"
|
||||
- $ref: "#/components/schemas/JavascriptTransform"
|
||||
@@ -268,8 +330,10 @@ components:
|
||||
|
||||
StaticTransform:
|
||||
type: object
|
||||
description: Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'
|
||||
properties:
|
||||
value: {}
|
||||
value:
|
||||
description: The static value. For resources, use format '$res:path/to/resource'
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
@@ -279,9 +343,11 @@ components:
|
||||
|
||||
JavascriptTransform:
|
||||
type: object
|
||||
description: JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value
|
||||
properties:
|
||||
expr:
|
||||
type: string
|
||||
description: JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
@@ -291,6 +357,7 @@ components:
|
||||
- type
|
||||
|
||||
FlowModuleValue:
|
||||
description: The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/RawScript"
|
||||
- $ref: "#/components/schemas/PathScript"
|
||||
@@ -316,16 +383,20 @@ components:
|
||||
|
||||
RawScript:
|
||||
type: object
|
||||
description: Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms
|
||||
properties:
|
||||
# to be made required once migration is over
|
||||
input_transforms:
|
||||
type: object
|
||||
description: Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
content:
|
||||
type: string
|
||||
description: The script source code. Should export a 'main' function
|
||||
language:
|
||||
type: string
|
||||
description: Programming language for this script
|
||||
enum:
|
||||
- deno
|
||||
- bun
|
||||
@@ -345,24 +416,32 @@ components:
|
||||
# TODO: Add missing languages
|
||||
path:
|
||||
type: string
|
||||
description: Optional path for saving this script
|
||||
lock:
|
||||
type: string
|
||||
description: Lock file content for dependencies
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- rawscript
|
||||
tag:
|
||||
type: string
|
||||
description: Worker group tag for execution routing
|
||||
concurrent_limit:
|
||||
type: number
|
||||
description: Maximum concurrent executions of this script
|
||||
concurrency_time_window_s:
|
||||
type: number
|
||||
description: Time window for concurrent_limit
|
||||
custom_concurrency_key:
|
||||
type: string
|
||||
description: Custom key for grouping concurrent executions
|
||||
is_trigger:
|
||||
type: boolean
|
||||
description: If true, this script is a trigger that can start the flow
|
||||
assets:
|
||||
type: array
|
||||
description: External resources this script accesses (S3 objects, resources, etc.)
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
@@ -371,8 +450,10 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: Path to the asset
|
||||
kind:
|
||||
type: string
|
||||
description: Type of asset
|
||||
enum:
|
||||
- s3object
|
||||
- resource
|
||||
@@ -380,9 +461,11 @@ components:
|
||||
- datatable
|
||||
access_type:
|
||||
type: string
|
||||
description: Access level for this asset
|
||||
enum: [r, w, rw]
|
||||
alt_access_type:
|
||||
type: string
|
||||
description: Alternative access level
|
||||
enum: [r, w, rw]
|
||||
required:
|
||||
- type
|
||||
@@ -392,23 +475,29 @@ components:
|
||||
|
||||
PathScript:
|
||||
type: object
|
||||
description: Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code
|
||||
properties:
|
||||
input_transforms:
|
||||
type: object
|
||||
description: Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
path:
|
||||
type: string
|
||||
description: Path to the script in the workspace (e.g., 'f/scripts/send_email')
|
||||
hash:
|
||||
type: string
|
||||
description: Optional specific version hash of the script to use
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
tag_override:
|
||||
type: string
|
||||
description: Override the script's default worker group tag
|
||||
is_trigger:
|
||||
type: boolean
|
||||
description: If true, this script is a trigger that can start the flow
|
||||
required:
|
||||
- type
|
||||
- path
|
||||
@@ -416,13 +505,16 @@ components:
|
||||
|
||||
PathFlow:
|
||||
type: object
|
||||
description: Reference to an existing flow by path. Use this to call another flow as a subflow
|
||||
properties:
|
||||
input_transforms:
|
||||
type: object
|
||||
description: Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
path:
|
||||
type: string
|
||||
description: Path to the flow in the workspace (e.g., 'f/flows/process_user')
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
@@ -434,22 +526,28 @@ components:
|
||||
|
||||
ForloopFlow:
|
||||
type: object
|
||||
description: Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations
|
||||
properties:
|
||||
modules:
|
||||
type: array
|
||||
description: Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'
|
||||
items:
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
iterator:
|
||||
description: JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
skip_failures:
|
||||
type: boolean
|
||||
description: If true, iteration failures don't stop the loop. Failed iterations return null
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- forloopflow
|
||||
parallel:
|
||||
type: boolean
|
||||
description: If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency
|
||||
parallelism:
|
||||
description: Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
squash:
|
||||
type: boolean
|
||||
@@ -461,20 +559,25 @@ components:
|
||||
|
||||
WhileloopFlow:
|
||||
type: object
|
||||
description: Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination
|
||||
properties:
|
||||
modules:
|
||||
type: array
|
||||
description: Steps to execute in each iteration. Use stop_after_if to control when the loop ends
|
||||
items:
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
skip_failures:
|
||||
type: boolean
|
||||
description: If true, iteration failures don't stop the loop. Failed iterations return null
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- whileloopflow
|
||||
parallel:
|
||||
type: boolean
|
||||
description: If true, iterations run concurrently (use with caution in while loops)
|
||||
parallelism:
|
||||
description: Maximum number of concurrent iterations when parallel=true
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
squash:
|
||||
type: boolean
|
||||
@@ -485,18 +588,23 @@ components:
|
||||
|
||||
BranchOne:
|
||||
type: object
|
||||
description: Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes
|
||||
properties:
|
||||
branches:
|
||||
type: array
|
||||
description: Array of branches to evaluate in order. The first branch with expr evaluating to true executes
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
summary:
|
||||
type: string
|
||||
description: Short description of this branch condition
|
||||
expr:
|
||||
type: string
|
||||
description: JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins
|
||||
modules:
|
||||
type: array
|
||||
description: Steps to execute if this branch's expr is true
|
||||
items:
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
required:
|
||||
@@ -504,9 +612,9 @@ components:
|
||||
- expr
|
||||
default:
|
||||
type: array
|
||||
description: Steps to execute if no branch expressions match
|
||||
items:
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
required: [modules]
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
@@ -518,18 +626,23 @@ components:
|
||||
|
||||
BranchAll:
|
||||
type: object
|
||||
description: Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently
|
||||
properties:
|
||||
branches:
|
||||
type: array
|
||||
description: Array of branches that all execute (either in parallel or sequentially)
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
summary:
|
||||
type: string
|
||||
description: Short description of this branch's purpose
|
||||
skip_failure:
|
||||
type: boolean
|
||||
description: If true, failure in this branch doesn't fail the entire flow
|
||||
modules:
|
||||
type: array
|
||||
description: Steps to execute in this branch
|
||||
items:
|
||||
$ref: "#/components/schemas/FlowModule"
|
||||
required:
|
||||
@@ -540,17 +653,21 @@ components:
|
||||
- branchall
|
||||
parallel:
|
||||
type: boolean
|
||||
description: If true, all branches execute concurrently. If false, they execute sequentially
|
||||
required:
|
||||
- branches
|
||||
- type
|
||||
|
||||
AgentTool:
|
||||
type: object
|
||||
description: A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')
|
||||
summary:
|
||||
type: string
|
||||
description: Short description of what this tool does (shown to the AI)
|
||||
value:
|
||||
$ref: "#/components/schemas/ToolValue"
|
||||
required:
|
||||
@@ -558,6 +675,7 @@ components:
|
||||
- value
|
||||
|
||||
ToolValue:
|
||||
description: The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/FlowModuleTool"
|
||||
- $ref: "#/components/schemas/McpToolValue"
|
||||
@@ -568,6 +686,7 @@ components:
|
||||
mcp: "#/components/schemas/McpToolValue"
|
||||
|
||||
FlowModuleTool:
|
||||
description: A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
@@ -581,6 +700,7 @@ components:
|
||||
|
||||
McpToolValue:
|
||||
type: object
|
||||
description: Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers
|
||||
properties:
|
||||
tool_type:
|
||||
type: string
|
||||
@@ -588,12 +708,15 @@ components:
|
||||
- mcp
|
||||
resource_path:
|
||||
type: string
|
||||
description: Path to the MCP resource/server configuration
|
||||
include_tools:
|
||||
type: array
|
||||
description: Whitelist of specific tools to include from this MCP server
|
||||
items:
|
||||
type: string
|
||||
exclude_tools:
|
||||
type: array
|
||||
description: Blacklist of tools to exclude from this MCP server
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
@@ -602,13 +725,39 @@ components:
|
||||
|
||||
AiAgent:
|
||||
type: object
|
||||
description: AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task
|
||||
properties:
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
description: Input parameters for the AI agent mapped to their values
|
||||
properties:
|
||||
provider:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
output_type:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
user_message:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
system_prompt:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
streaming:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
messages_context_length:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
output_schema:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
user_images:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
max_completion_tokens:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
temperature:
|
||||
$ref: "#/components/schemas/InputTransform"
|
||||
required:
|
||||
- provider
|
||||
- user_message
|
||||
- output_type
|
||||
tools:
|
||||
type: array
|
||||
description: Array of tools the agent can use. The agent decides which tools to call based on the task
|
||||
items:
|
||||
$ref: "#/components/schemas/AgentTool"
|
||||
type:
|
||||
@@ -617,6 +766,7 @@ components:
|
||||
- aiagent
|
||||
parallel:
|
||||
type: boolean
|
||||
description: If true, the agent can execute multiple tool calls in parallel
|
||||
required:
|
||||
- tools
|
||||
- type
|
||||
@@ -624,6 +774,7 @@ components:
|
||||
|
||||
Identity:
|
||||
type: object
|
||||
description: Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
@@ -631,6 +782,7 @@ components:
|
||||
- identity
|
||||
flow:
|
||||
type: boolean
|
||||
description: If true, marks this as a flow identity (special handling)
|
||||
|
||||
required:
|
||||
- type
|
||||
|
||||
Reference in New Issue
Block a user