Currently, when an AI edit request comes in, we wipe the entire existing graph and start afresh. This creates undesired UX effects:
- Visual jumps in graph/node positioning
- Loss of sidebar selection state
- Jarring experience for simple edits like "make the rectangle bigger"
The core challenge: Node IDs change between edits, making it difficult to identify "the same" or "similar" nodes.
- ID Mapping: Creates
idMap: [String : UUID]to track AI string IDs to actual node UUIDs - Node Reuse Check: For each AI node, checks if existing node with same ID has compatible type
- Selective Creation: Only creates new nodes when type doesn't match
- Bulk Deletion: Critical Issue - Deletes ALL nodes not mentioned in AI response (lines 372-385)
- AI generates random UUIDs: Each generation creates new UUIDs (not deterministic)
- Variable naming pattern:
{patchTypeName}_{nodeUUID}(e.g.,addPatch_123-abc-456) - Layer variables:
layer_{layerUUID}_{gestureName}
- Uses topological sorting (Kahn's algorithm) to determine depth levels
- Positions nodes in columns based on depth
- Centers entire chain around viewport
- Critical Issue: Dictionary/Set iteration order causes non-deterministic positioning within same depth level
- Nodes at same topological depth are processed in arbitrary order
- This causes position differences even for identical graphs
- Root cause: Swift Set iteration is unordered
- Makes position-based node matching unreliable
- "make rectangle bigger" - only size changes
- "switch from blue to green" → "switch from yellow to red" - only colors change
- Preserved: Node types, count, topology, connection structure
- "oval" → "rectangle" - shape type changes but size/color preserved
- Preserved: Node count, topology, most parameters
- "one oval" → "100 draggable rectangles"
- Changed: Everything - node count, topology, parameters
Approach: Match old nodes with new nodes based on similarity scoring
Matching Criteria (in priority order):
- Exact Match: Same NodeKind + same title + same input values
- Type + Title Match: Same NodeKind + same title (ignoring values)
- Type Match: Same NodeKind for common types
- Connection Pattern: Similar upstream/downstream relationships
Implementation:
struct NodeSimilarityMatcher {
func findBestMatch(oldNode: NodeViewModel, newNodes: [NodeViewModel]) -> NodeViewModel?
func calculateSimilarity(node1: NodeViewModel, node2: NodeViewModel) -> Double
}Approach: Capture state before apply, restore after
- Snapshot all node positions and sidebar selection
- After graph creation, restore for matched nodes
- Use fuzzy matching based on properties
Approach: Diff and apply changes instead of wholesale replacement
- Identify nodes to: keep, update, add, delete
- Apply changes incrementally
- Most complex but most preserving
Approach: Have AI explicitly mark which nodes to preserve
- Modify prompts to include preservation hints
- AI returns old→new ID mapping
- Most accurate but requires AI changes
- Line-based comparison: Git diff, unified diff use line-by-line comparison
- LCS (Longest Common Subsequence): Maximize unchanged parts
- Myers Algorithm: Used by Git, optimal for finding minimal edits
- Key principle: Match based on content similarity, not identity
- Nodes are like "lines" but with richer structure
- Can't rely on position (since it's recalculated)
- Must match on semantic properties instead of syntactic position
- Similar to how semantic diff tools compare ASTs rather than text
- Create
NodeSimilarityMatcherwith configurable matching rules - Score similarity based on:
- Node type (patch vs layer)
- Patch/layer specific type
- Input port configuration
- Constant values
- Connection patterns
- Before creating new nodes, run matching algorithm
- Update
idMapto reuse existing IDs for matched nodes - Preserve positions for matched nodes
- Only delete truly obsolete nodes (no matches found)
- Track selected layer IDs before update
- Map old IDs to new IDs via matching
- Restore selection using mapped IDs
NodeViewModel.kind: Node type (patch/layer/group/component)NodeViewModel.title: Display titleNodeViewModel.inputs/outputs: Port values- Connection topology via
InputNodeRowObserver.upstreamOutputCoordinate - Position via
CanvasItemViewModel.position
idMapsystem for ID tracking- Node reuse logic (lines 183-184 check for
needsNewNodeCreation) - Position preservation when nodes are reused
- Sidebar update mechanism via
LayersSidebarViewModel.update()
- Matching Confidence Threshold: What similarity score is "good enough" to reuse a node?
- Multiple Matches: If multiple new nodes match one old node, how to choose?
- Orphaned Nodes: Should we keep nodes not in the new graph but mark them somehow?
- Performance: How expensive is similarity calculation for large graphs?
Within the same topological depth level, nodes are processed in whatever order the dictionary/set iteration provides, which is non-deterministic.
Approach: Use the order nodes appear in the AI's SwiftUI code generation
The AI generates nodes in a specific order:
javascript_patchesarraynative_patchesarraylayer_data_listarray
Implementation: Add creation order index when processing these arrays, then sort nodes at same depth level by this order.
Approach: Sort by meaningful node properties
- By node type: Sort patch types alphabetically ("Add", "Multiply", "Value")
- By layer hierarchy: Layers before patches, parent layers before children
- By connection role: Sources before processors before sinks
Approach: Create deterministic but arbitrary ordering
- Sort by UUID string representation (deterministic across runs)
- Sort by node title/display name
- Sort by stable hash of node properties (type + input values)
Approach: Use graph structure for sub-ordering
- Nodes with more upstream connections first
- Nodes with more downstream connections last
- Nodes with similar connection patterns grouped together
- Primary: AI generation order (preserves intent)
- Secondary: Node type alphabetical (semantic meaning)
- Tertiary: UUID string (deterministic fallback)
- Added nodeCreationOrder tracking - All nodes (JS patches, native patches, layers) are assigned creation order
- Enhanced positionAIGeneratedNodesDuringApply - Sorts nodes at same depth level by creation order + fallback to UUID
- Deterministic layout - Identical graphs now produce consistent positioning
- Created NodeSimilarityMatcher with sophisticated scoring:
- Type match (3.0 points) - Exact patch/layer type matching
- Input values (2.5 points) - Value structure and content comparison
- Connection patterns (2.0 points) - Upstream/downstream connection analysis
- Category match (0.5 points) - Both patches or both layers
- Enhanced matching logic with proper PortValue comparison using Equatable
- Selective preservation - Only deletes truly unmatched nodes
- Sidebar selection - Preserves LayersSidebarViewModel.primary and lastFocused
- Canvas selection - Preserves GraphUISelectionState.selectedCanvasItems (orange borders)
- Intelligent mapping - Maps canvas items to their owner nodes for preservation
- Added isPatch/isLayer to PatchOrLayer enum for cleaner type checking
The system now provides:
- No visual jumps - Similar nodes keep positions through deterministic sorting
- Smart preservation - Nodes are reused when appropriate based on sophisticated similarity scoring
- Complete state preservation - Both sidebar and canvas selections maintained
- Robust matching - Connection patterns and input values improve accuracy
- ✅ "Make rectangle bigger" - Rectangle preserved, size updated
- ✅ "Switch from blue to green" - Same nodes, different values
- ✅ "Oval to rectangle" - May preserve position if similarity high enough
- ✅ Complex restructuring - Creates new nodes as needed, preserves what's similar
The implementation successfully eliminates jarring UX issues during AI graph updates.
- Start with conservative matching (high confidence only)
- Log matching decisions for debugging
- Consider making matching configurable per edit type
- May need special handling for group nodes and components
- Position preservation should account for new nodes needing space
- Current implementation:
/Stitch/Graph/StitchAI/GraphPrompting/Model/RequestTypes/AIPatchBuilderRequest.swift - Node positioning:
/Stitch/Graph/StitchAI/GraphPrompting/Model/StepApplication.swift - Node structure:
/Stitch/Graph/ViewModel/GraphState.swift - Sidebar management:
/Stitch/Graph/Sidebar/LayersSidebar/LayersSidebarViewModel.swift