-
Notifications
You must be signed in to change notification settings - Fork 852
feat(aichat): simplify flow mode edits #6981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Deploying windmill with
|
| Latest commit: |
b56fc92
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://306bc27e.windmill.pages.dev |
| Branch Preview URL: | https://fg-flowchat.windmill.pages.dev |
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>
- 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>
- 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>
- 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>
- 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>
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 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>
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>
|
/ai-fast review this PR |
|
I'll analyze this and get back to you. |
|
Claude finished @centdix's task —— View job Code Review: feat(aichat): simplify flow mode editsThis PR represents a significant architectural improvement to the AI chat flow editing system. The refactoring successfully extracts diff management into a reusable store and simplifies the main FlowAIChat component considerably. Summary of ChangesMajor Additions:
Major Refactorings:
Architectural Review ⭐⭐⭐⭐ (4/5)Strengths:
Areas for Improvement:
Code Quality Issues🔴 Critical Issues
🟡 Important Issues
🔵 Minor Issues
Svelte 5 Best Practices ⭐⭐⭐⭐⭐ (5/5)✅ Excellent Svelte 5 compliance:
Security Review ⭐⭐⭐⭐ (4/5)No major security vulnerabilities found, but note:
Performance Review ⭐⭐⭐ (3/5)Areas of Concern:
Recommendations:
Testing RecommendationsGiven the complexity of these changes, recommend testing:
Reproduction Instructions for TestersTo test the new flow AI chat diff management features: Navigate to a flow editor in the Windmill app and open the AI chat panel. Ask the AI to make changes to your flow such as adding new steps, modifying existing steps, or removing steps. The AI will now show pending changes with accept/reject buttons directly on the flow graph nodes. Click individual accept or reject buttons to approve or decline specific changes, or use the bulk accept/reject all buttons in the chat interface. Modified modules will show a "Diff" button that opens a side-by-side comparison view. The system now tracks changes across the entire flow including nested modules in loops and branches, and you can accept or reject changes granularly. Test particularly with nested structures like modules inside for-loops or branch conditions, and verify that the diff tracking correctly shows which modules were added, modified, or removed even when they're deeply nested in the flow hierarchy. Overall Assessment ⭐⭐⭐⭐ (4/5)This is a high-quality refactoring with significant architectural improvements. The separation of concerns is excellent, the code is cleaner and more maintainable, and the Svelte 5 implementation is exemplary. Recommendation: Approve with minor fixes required The critical issues identified (circular reference bugs, incomplete ID checking) should be addressed before merging, but they're relatively straightforward fixes. The performance concerns are noted but acceptable for initial release - they can be optimized based on real-world usage patterns. Estimated effort to fix critical issues: 2-4 hours |
| parentLoc.type !== 'root' && | ||
| parentLoc.type !== 'failure' && | ||
| parentLoc.type !== 'preprocessor' | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential infinite recursion issue: This function could infinitely recurse if the parent structure creates a loop. Consider adding a visited set or depth limit to prevent stack overflow in malformed flow structures.
function acceptModule(
id: string,
flowStore?: StateStore<ExtendedOpenFlow>,
asSkeleton = false,
visited = new Set<string>() // Add this
) {
if (visited.has(id)) {
throw new Error(`Circular dependency detected for module ${id}`)
}
visited.add(id)
// ... rest of function
}| insertIntoNestedParent(merged, parentLocation, clonedModule, beforeFlow) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential logic error: The condition checks if branchIndex is out of bounds, but this happens AFTER checking module.id === containerId. If the module is found but the branch index is invalid, the error message could be confusing since it might refer to a different module's branches that was found deeper in recursion.
Consider adding a check to verify the module exists AND is a branchall/branchone BEFORE checking branch bounds:
if (module.id === containerId) {
if (module.value.type !== 'branchall' && module.value.type !== 'branchone') {
throw new Error(`Module '${containerId}' is not a branchall/branchone container`)
}
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})`)
}
// ... rest
}
}| // When adding a branch (not a module), skip ID checks and inline script handling | ||
| if (!isAddingNewBranch) { | ||
| // Check for duplicate IDs (including nested modules) | ||
| const allNewIds = collectAllModuleIds(processedValue as FlowModule) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ID uniqueness check may miss duplicates in nested structures: The duplicate ID check only looks at flow.value.modules, but doesn't check against preprocessor_module or failure_module. This could allow duplicate IDs if adding a module with the same ID as the preprocessor or failure handler.
Consider checking all modules:
// Check for duplicate IDs (including nested modules AND special modules)
const allNewIds = collectAllModuleIds(processedValue as FlowModule)
const allExistingModules = getAllModulesMap(flow.value) // from flowDiff.ts
for (const newId of allNewIds) {
if (allExistingModules.has(newId)) {
throw new Error(
`Module with id '${newId}' already exists in the flow. Each module ID must be unique.`
)
}
}| .describe('Maximum number of parallel iterations (null to not change)') | ||
| }) | ||
| // Recursively resolve the referenced schema | ||
| return resolveSchemaRefs(resolved, rootSchema, new Set(visited)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circular reference handling is incomplete: The function creates a new Set(visited) when recursing, which defeats the purpose of tracking visited nodes. Each recursive call gets a fresh copy of the visited set, so circular references won't be detected.
Fix:
// Don't create a new Set - pass the same one
return resolveSchemaRefs(resolved, rootSchema, visited) // Remove 'new Set(visited)'| const hasPendingChanges = $derived(Object.values(moduleActions).some((info) => info.pending)) | ||
|
|
||
| // Auto-compute diff when beforeFlow or currentFlow changes | ||
| $effect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Effect cleanup missing: This $effect doesn't have explicit cleanup. While it's recomputing based on reactive dependencies, if buildFlowTimeline creates any subscriptions or side effects internally, they won't be cleaned up.
Consider adding explicit cleanup:
$effect(() => {
if (beforeFlow && currentFlow) {
// ... existing code
} else if (!beforeFlow) {
// ... existing code
}
return () => {
// Cleanup if needed
}
})| ${JSON.stringify(flow.schema ?? emptySchema())} | ||
| // Clear the inline script store and extract inline scripts for token optimization | ||
| inlineScriptStore.clear() | ||
| const optimizedModules = extractAndReplaceInlineScripts(flow.value.modules) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for module extraction: If extractAndReplaceInlineScripts encounters a malformed module structure, it could fail silently or return unexpected results. Consider adding validation or try-catch to ensure robustness:
try {
const optimizedModules = extractAndReplaceInlineScripts(flow.value.modules)
// ... rest of code
} catch (error) {
console.error('Failed to optimize inline scripts:', error)
// Fallback to using unoptimized modules
const flowModulesYaml = applyCodePiecesToFlowModules(codePieces, flow.value.modules)
}| selectNextIdFn?: (id: string) => void | ||
| ): boolean { | ||
| selectNextIdFn?.(id) | ||
| const deleted = deleteModuleInternal(id, flowStore.val) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition potential: If deleteModuleFromFlow is called multiple times rapidly (e.g., user clicks delete button multiple times), the refreshStateStore calls could race and lead to inconsistent state. Consider debouncing or adding a lock:
let isDeleting = false
function deleteModuleFromFlow(
id: string,
flowStore: StateStore<ExtendedOpenFlow>,
selectNextIdFn?: (id: string) => void
): boolean {
if (isDeleting) return false
isDeleting = true
try {
selectNextIdFn?.(id)
const deleted = deleteModuleInternal(id, flowStore.val)
if (deleted) {
refreshStateStore(flowStore)
}
return deleted
} finally {
isDeleting = false
}
}| for (const id of Object.keys(untrackedAffectedModules)) { | ||
| if (!allModuleIds.includes(id)) { | ||
| delete affectedModules[id] | ||
| if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Untracked effect dependencies: The $effect at line 231-252 uses untrack() which prevents the effect from re-running when $currentEditor changes. However, the effect body accesses $currentEditor.editor.reviewAppliedCode() and $currentEditor.hideDiffMode(). If $currentEditor becomes undefined or changes while the effect is running, this could throw errors.
Consider adding null checks:
untrack(() => {
if ($currentEditor?.editor?.reviewAppliedCode) {
$currentEditor.editor.reviewAppliedCode(content, {
onFinishedReview: () => {
diffManager?.acceptModule(selectedId, flowStore)
$currentEditor?.hideDiffMode?.()
}
})
}
})| beforeActions: Record<string, ModuleActionInfo> | ||
| ): FlowValue { | ||
| // Deep clone afterFlow to avoid mutation | ||
| const merged: FlowValue = JSON.parse(JSON.stringify(afterFlow)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deep clone using JSON.parse/stringify: Same concern as the cloneModule function. This is used to clone the entire afterFlow, which could be a very large object. Consider:
- Adding a size check and warning if the flow is very large
- Using a streaming approach for very large flows
- Documenting the performance characteristics
function reconstructMergedFlow(
afterFlow: FlowValue,
beforeFlow: FlowValue,
beforeActions: Record<string, ModuleActionInfo>
): FlowValue {
// Warn if flow is very large (> 10MB when stringified)
const flowStr = JSON.stringify(afterFlow)
if (flowStr.length > 10_000_000) {
console.warn('Large flow detected, deep clone may be slow:', flowStr.length)
}
const merged: FlowValue = JSON.parse(flowStr)
// ...
}| const newModule = { ...module } | ||
|
|
||
| if (newModule.value.type === 'rawscript' && newModule.value.content) { | ||
| // Store the original content |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline script store is a module-level singleton: The inlineScriptStore is a module-level singleton that persists across component instances. This could lead to memory leaks or stale data if multiple flows are edited in different tabs/components. Consider:
- Making it a per-flow instance
- Adding a
clear()call when the flow editor is unmounted - Adding size limits to prevent unbounded growth
// Add size limit
class InlineScriptStore {
private scripts: Map<string, string> = new Map()
private maxSize = 1000 // Prevent memory leaks
set(moduleId: string, content: string) {
if (this.scripts.size >= this.maxSize && !this.scripts.has(moduleId)) {
console.warn('InlineScriptStore max size reached, clearing oldest entries')
// Clear oldest half
const entries = Array.from(this.scripts.entries())
entries.slice(0, this.maxSize / 2).forEach(([key]) => this.scripts.delete(key))
}
this.scripts.set(moduleId, content)
}
// ...
}| if (existingModule) { | ||
| // Module already exists (as skeleton or partial), update it in-place | ||
| const moduleToApply = asSkeleton ? createSkeletonModule(module) : module | ||
| Object.keys(existingModule).forEach((k) => delete (existingModule as any)[k]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object.assign with $state could lose reactivity: Using Object.assign to update reactive state objects might not trigger reactivity properly in all cases. Consider using direct property assignment or spread operator:
// Instead of:
Object.assign(existingModule, $state.snapshot(moduleToApply))
// Consider:
for (const [key, value] of Object.entries($state.snapshot(moduleToApply))) {
(existingModule as any)[key] = value
}Or better yet, ensure the entire parent object is reassigned to trigger reactivity.
| afterActions[moduleId] = { action: 'added', pending: options.markAsPending } | ||
| } else if ( | ||
| !deepEqual( | ||
| normalizeModuleForComparison(beforeModule), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deep equality check could be expensive: Using deepEqual from fast-equals on every module comparison could be a performance bottleneck for flows with many modules. Consider:
- Adding a quick shallow check first (e.g., comparing hash/timestamp if available)
- Caching comparison results
- Using a more targeted comparison that only checks relevant fields
// Quick shallow check first
if (beforeModule.id === afterModule.id &&
beforeModule.summary === afterModule.summary) {
// Only do deep comparison if quick check suggests changes
if (!deepEqual(
normalizeModuleForComparison(beforeModule),
normalizeModuleForComparison(afterModule)
)) {
// Modified
}
}| } | ||
| // Check for restricted IDs | ||
| if (isRestrictedModuleId(value.id)) { | ||
| throw new Error(`Restricted id '${value.id}', can't be used, should choose an other`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: In the error message, "should choose an other" should be "should choose another".
| throw new Error(`Restricted id '${value.id}', can't be used, should choose an other`) | |
| throw new Error(`Restricted id '${value.id}', can't be used, should choose another`) |
* 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

No description provided.