Skip to content

Conversation

@centdix
Copy link
Collaborator

@centdix centdix commented Oct 28, 2025

No description provided.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 28, 2025

Deploying windmill with  Cloudflare Pages  Cloudflare Pages

Latest commit: b56fc92
Status: ✅  Deploy successful!
Preview URL: https://306bc27e.windmill.pages.dev
Branch Preview URL: https://fg-flowchat.windmill.pages.dev

View logs

Base automatically changed from fg/diffvisualizer to main November 1, 2025 14:17
centdix and others added 6 commits November 1, 2025 21:06
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>
centdix and others added 20 commits November 1, 2025 21:18
- 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>
@centdix
Copy link
Collaborator Author

centdix commented Dec 1, 2025

/ai-fast review this PR

@claude
Copy link
Contributor

claude bot commented Dec 1, 2025

Claude Code is working…

I'll analyze this and get back to you.

View job run

@centdix centdix marked this pull request as ready for review December 1, 2025 14:34
@claude
Copy link
Contributor

claude bot commented Dec 1, 2025

Claude finished @centdix's task —— View job


Code Review: feat(aichat): simplify flow mode edits

This 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 Changes

Major Additions:

  • ✅ New flowDiffManager.svelte.ts - Reusable Svelte 5 rune-based diff management store (~515 lines)
  • ✅ New DiffActionBar.svelte - Reusable accept/reject UI component (~65 lines)
  • ✅ Enhanced flowDiff.ts - Added timeline building, duplicate ID handling, and merge reconstruction (+313 lines)
  • ✅ New openFlow.json - OpenAPI schema for flow structure validation

Major Refactorings:

  • FlowAIChat.svelte - Simplified from ~533 to ~157 lines (-376 lines, -70% reduction)
  • core.ts - Significant refactoring with inline script optimization (+1870/-673 lines)
  • ✅ Deleted ModuleAcceptReject.svelte - Functionality moved to DiffActionBar

Architectural Review ⭐⭐⭐⭐ (4/5)

Strengths:

  1. Excellent separation of concerns - DiffManager is now independent and reusable
  2. Improved maintainability - FlowAIChat is much cleaner and easier to understand
  3. Better state management - Using Svelte 5 runes effectively for reactive diff tracking
  4. Token optimization - Inline script reference system reduces AI token usage significantly
  5. Comprehensive diff handling - Handles nested modules, moved modules, and type changes correctly

Areas for Improvement:

  1. Memory management - InlineScriptStore is a singleton without size limits (could leak memory)
  2. Performance - Deep cloning with JSON.parse/stringify could be slow for large flows
  3. Error handling - Some edge cases lack proper error recovery

Code Quality Issues

🔴 Critical Issues

  1. Circular reference not prevented (flowDiffManager.svelte.ts:273)

    • acceptModule could infinitely recurse with malformed parent structures
    • Need to add visited set or depth limit
  2. Circular reference detection broken (core.ts:136)

    • resolveSchemaRefs creates new Set(visited) defeating circular detection
    • Should pass the same visited set through recursion
  3. Incomplete duplicate ID check (core.ts:1626)

    • Only checks flow.value.modules, misses preprocessor_module and failure_module
    • Could allow duplicate IDs in special modules

🟡 Important Issues

  1. Singleton memory leak risk (core.ts:1068)

    • inlineScriptStore persists across all flow instances
    • No size limits or cleanup mechanism
    • Recommend per-instance store or max size limit
  2. Race condition potential (flowDiffManager.svelte.ts:232)

    • Multiple rapid deleteModuleFromFlow calls could race
    • Consider adding debouncing or locking
  3. Performance concern (flowDiff.ts:469, 257)

    • Deep cloning entire flows with JSON.parse(JSON.stringify())
    • Could be slow for large flows (>10MB)
    • Consider size warnings or streaming approach
  4. Deep equality performance (flowDiff.ts:107)

    • Running deepEqual on every module could be expensive
    • Consider quick shallow checks first
  5. Untracked effect safety (FlowAIChat.svelte:232)

    • Accessing $currentEditor inside untrack() without null checks
    • Could throw if editor becomes undefined mid-execution

🔵 Minor Issues

  1. Effect cleanup missing (flowDiffManager.svelte.ts:84)

    • $effect doesn't return cleanup function
    • Could leave dangling subscriptions
  2. Object.assign reactivity concern (flowDiffManager.svelte.ts:295)

    • Using Object.assign with $state might not trigger reactivity properly
    • Consider direct property assignment
  3. Missing error handling (core.ts:2219)

    • extractAndReplaceInlineScripts could fail silently
    • Add try-catch for robustness
  4. Mutation of input (shared.ts:206)

    • applyCodePiecesToFlowModules modifies input array
    • Should document or work on copy

Svelte 5 Best Practices ⭐⭐⭐⭐⭐ (5/5)

Excellent Svelte 5 compliance:

  • Proper use of $state, $derived, and $effect runes
  • Good reactive programming patterns
  • Appropriate use of $state.snapshot() for non-reactive copies
  • Clean component structure with $props()

Security Review ⭐⭐⭐⭐ (4/5)

No major security vulnerabilities found, but note:

  • ⚠️ JSON.parse could fail with malicious input (mitigated by type system)
  • ⚠️ Unbounded memory growth in InlineScriptStore (DoS risk with many flows)
  • ✅ Input validation is good
  • ✅ No XSS vulnerabilities introduced

Performance Review ⭐⭐⭐ (3/5)

Areas of Concern:

  1. Deep cloning with JSON.parse/stringify for large objects
  2. No caching for repeated diff computations
  3. Deep equality checks on every module
  4. Multiple array iterations in extractAllModules and similar functions

Recommendations:

  • Add size warnings for large flows
  • Implement shallow equality checks before deep comparisons
  • Consider memoization for expensive operations
  • Add performance monitoring/metrics

Testing Recommendations

Given the complexity of these changes, recommend testing:

  1. Nested module operations - Add/remove/modify modules deep in branches/loops
  2. Edge cases - Empty flows, single module, very large flows (100+ modules)
  3. Concurrent operations - Rapid accept/reject clicks, simultaneous edits
  4. Memory leaks - Open/close multiple flows, check InlineScriptStore growth
  5. Type changes - Changing module types (script → flow, etc.)
  6. Special modules - Preprocessor and failure handler modifications
  7. Circular structures - Malformed flows with circular references (error handling)

Reproduction Instructions for Testers

To 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'
) {
Copy link
Contributor

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)
}
}

Copy link
Contributor

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)
Copy link
Contributor

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))
Copy link
Contributor

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(() => {
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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 (
Copy link
Contributor

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))
Copy link
Contributor

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:

  1. Adding a size check and warning if the flow is very large
  2. Using a streaming approach for very large flows
  3. 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
Copy link
Contributor

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:

  1. Making it a per-flow instance
  2. Adding a clear() call when the flow editor is unmounted
  3. 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])
Copy link
Contributor

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),
Copy link
Contributor

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:

  1. Adding a quick shallow check first (e.g., comparing hash/timestamp if available)
  2. Caching comparison results
  3. 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`)
Copy link
Contributor

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".

Suggested change
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`)

@centdix centdix marked this pull request as draft December 1, 2025 15:29
centdix and others added 6 commits December 3, 2025 11:01
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants