/ src / store / interbrain-store.ts
interbrain-store.ts
   1  import { create } from 'zustand';
   2  import { persist } from 'zustand/middleware';
   3  import { DreamNode } from '../types/dreamnode';
   4  import { DreamSongData } from '../types/dreamsong';
   5  import { FibonacciSphereConfig, DEFAULT_FIBONACCI_CONFIG } from '../dreamspace/FibonacciSphereLayout';
   6  import { MockDataConfig } from '../mock/dreamnode-mock-data';
   7  import {
   8    OllamaConfigSlice,
   9    createOllamaConfigSlice,
  10    extractOllamaPersistenceData,
  11    restoreOllamaPersistenceData,
  12    OllamaConfig
  13  } from '../features/semantic-search/store/ollama-config-slice';
  14  // OllamaConfig imports are in the semantic search slice
  15  import { VectorData } from '../features/semantic-search/services/indexing-service';
  16  import { FlipState } from '../types/dreamsong';
  17  import {
  18    DreamSongRelationshipGraph,
  19    SerializableDreamSongGraph,
  20    serializeRelationshipGraph,
  21    deserializeRelationshipGraph
  22  } from '../types/constellation';
  23  
  24  // Helper function to get current scroll position of DreamSong content
  25  function getDreamSongScrollPosition(nodeId: string): number | null {
  26    try {
  27      // Ensure we're in browser environment
  28      if (typeof document === 'undefined') return null;
  29      
  30      // Look for DreamSong leaf in right pane containing this nodeId
  31       
  32      const dreamSongLeaf = document.querySelector(`[data-type="dreamsong-fullscreen"][data-node-id="${nodeId}"]`);
  33      if (dreamSongLeaf) {
  34        const scrollContainer = dreamSongLeaf.querySelector('.dreamsong-content');
  35        if (scrollContainer && 'scrollTop' in scrollContainer) {
  36           
  37          return (scrollContainer as HTMLElement).scrollTop;
  38        }
  39      }
  40      
  41      // Also check for embedded DreamSong content in DreamSpace
  42       
  43      const dreamSpaceContent = document.querySelector(`.dreamsong-container[data-node-id="${nodeId}"] .dreamsong-content`);
  44      if (dreamSpaceContent && 'scrollTop' in dreamSpaceContent) {
  45         
  46        return (dreamSpaceContent as HTMLElement).scrollTop;
  47      }
  48      
  49      return null;
  50    } catch (error) {
  51      console.warn(`Failed to get scroll position for node ${nodeId}:`, error);
  52      return null;
  53    }
  54  }
  55  
  56  // Helper function to restore scroll position of DreamSong content
  57  function restoreDreamSongScrollPosition(nodeId: string, scrollPosition: number): void {
  58    try {
  59      // Ensure we're in browser environment
  60      if (typeof document === 'undefined') return;
  61      
  62      // Look for DreamSong leaf in right pane containing this nodeId
  63       
  64      const dreamSongLeaf = document.querySelector(`[data-type="dreamsong-fullscreen"][data-node-id="${nodeId}"]`);
  65      if (dreamSongLeaf) {
  66        const scrollContainer = dreamSongLeaf.querySelector('.dreamsong-content');
  67        if (scrollContainer && 'scrollTop' in scrollContainer) {
  68           
  69          (scrollContainer as HTMLElement).scrollTop = scrollPosition;
  70          return;
  71        }
  72      }
  73      
  74      // Also check for embedded DreamSong content in DreamSpace
  75       
  76      const dreamSpaceContent = document.querySelector(`.dreamsong-container[data-node-id="${nodeId}"] .dreamsong-content`);
  77      if (dreamSpaceContent && 'scrollTop' in dreamSpaceContent) {
  78         
  79        (dreamSpaceContent as HTMLElement).scrollTop = scrollPosition;
  80      }
  81    } catch (error) {
  82      console.warn(`Failed to restore scroll position for node ${nodeId}:`, error);
  83    }
  84  }
  85  
  86  // Navigation history types
  87  export interface NavigationHistoryEntry {
  88    /** Node ID that was focused (null for constellation view) */
  89    nodeId: string | null;
  90    /** Layout type at the time */
  91    layout: 'constellation' | 'liminal-web';
  92    /** Timestamp of the navigation action */
  93    timestamp: number;
  94    /** Flip state of the focused node (null if not flipped or no focused node) */
  95    flipState: FlipState | null;
  96    /** Scroll position in DreamSong content (null if not applicable) */
  97    scrollPosition: number | null;
  98  }
  99  
 100  export interface NavigationHistoryState {
 101    /** Stack of past navigation states */
 102    history: NavigationHistoryEntry[];
 103    /** Current position in history (0 = most recent) */
 104    currentIndex: number;
 105    /** Maximum history entries to keep */
 106    maxHistorySize: number;
 107  }
 108  
 109  // Creation state types
 110  export interface ProtoNode {
 111    title: string;
 112    type: 'dream' | 'dreamer';
 113    dreamTalkFile?: globalThis.File;
 114    additionalFiles?: globalThis.File[];
 115    position: [number, number, number];
 116    urlMetadata?: import('../utils/url-utils').UrlMetadata;
 117  }
 118  
 119  export interface ValidationErrors {
 120    title?: string;
 121    dreamTalk?: string;
 122  }
 123  
 124  export interface CreationState {
 125    isCreating: boolean;
 126    protoNode: ProtoNode | null;
 127    validationErrors: ValidationErrors;
 128  }
 129  
 130  // Edit mode state types
 131  export interface EditModeState {
 132    isActive: boolean;
 133    editingNode: DreamNode | null;
 134    originalRelationships: string[]; // Store original relationships for cancel operation
 135    pendingRelationships: string[]; // Track relationship changes
 136    searchResults: DreamNode[]; // Search results for relationship discovery
 137    validationErrors: EditModeValidationErrors; // Validation errors for edit mode
 138    newDreamTalkFile?: globalThis.File; // New media file for DreamTalk editing
 139    isSearchingRelationships: boolean; // Toggle state for relationship search interface
 140  }
 141  
 142  export interface EditModeValidationErrors {
 143    title?: string;
 144    dreamTalk?: string;
 145    relationships?: string;
 146  }
 147  
 148  // Copilot mode state types
 149  export interface CopilotModeState {
 150    isActive: boolean;
 151    conversationPartner: DreamNode | null; // The person node at center
 152    transcriptionFilePath: string | null; // Path to active transcription file
 153    showSearchResults: boolean; // Option key held state for showing/hiding results
 154    frozenSearchResults: DreamNode[]; // Snapshot of results when showing
 155    sharedNodeIds: string[]; // Track invoked nodes for post-call processing
 156  }
 157  
 158  // Real node storage - persisted across sessions
 159  export interface RealNodeData {
 160    node: DreamNode;
 161    fileHash?: string; // For detecting file changes
 162    lastSynced: number; // Timestamp of last vault sync
 163  }
 164  
 165  // DreamSong cache interface for service layer
 166  export interface DreamSongCacheEntry {
 167    data: DreamSongData;
 168    timestamp: number;
 169    structureHash: string;
 170  }
 171  
 172  // Note: OllamaConfig and DEFAULT_OLLAMA_CONFIG moved to semantic search feature
 173  
 174  export interface InterBrainState extends OllamaConfigSlice {
 175    // Data mode toggle
 176    dataMode: 'mock' | 'real';
 177    setDataMode: (mode: 'mock' | 'real') => void;
 178    
 179    // Real nodes storage (persisted)
 180    realNodes: Map<string, RealNodeData>;
 181    setRealNodes: (nodes: Map<string, RealNodeData>) => void;
 182    updateRealNode: (id: string, data: RealNodeData) => void;
 183    batchUpdateNodePositions: (positions: Map<string, [number, number, number]>) => void;
 184    deleteRealNode: (id: string) => void;
 185    
 186    // Selected DreamNode state
 187    selectedNode: DreamNode | null;
 188    setSelectedNode: (node: DreamNode | null) => void;
 189    
 190    // Selected DreamNode's DreamSong data (for reuse in full-screen)
 191    selectedNodeDreamSongData: DreamSongData | null;
 192    setSelectedNodeDreamSongData: (data: DreamSongData | null) => void;
 193    
 194    // DreamSong cache for service layer
 195    dreamSongCache: Map<string, DreamSongCacheEntry>;
 196    getCachedDreamSong: (nodeId: string, structureHash: string) => DreamSongCacheEntry | null;
 197    setCachedDreamSong: (nodeId: string, structureHash: string, data: DreamSongData) => void;
 198    
 199    // Creator mode state
 200    creatorMode: {
 201      isActive: boolean;
 202      nodeId: string | null; // ID of the node being edited
 203    };
 204    setCreatorMode: (active: boolean, nodeId?: string | null) => void;
 205    
 206    // Search functionality state
 207    searchResults: DreamNode[];
 208    setSearchResults: (results: DreamNode[]) => void;
 209    
 210    // Search interface state
 211    searchInterface: {
 212      isActive: boolean;
 213      isSaving: boolean; // Track if save animation is in progress
 214      currentQuery: string;
 215      lastQuery: string; // For change detection
 216    };
 217    setSearchActive: (active: boolean) => void;
 218    setSearchQuery: (query: string) => void;
 219    setSearchSaving: (saving: boolean) => void;
 220    
 221    // Spatial layout state - expanded to include edit modes as first-class states
 222    spatialLayout: 'constellation' | 'creation' | 'search' | 'liminal-web' | 'edit' | 'edit-search' | 'copilot';
 223    setSpatialLayout: (layout: 'constellation' | 'creation' | 'search' | 'liminal-web' | 'edit' | 'edit-search' | 'copilot') => void;
 224    
 225    // Fibonacci sphere layout configuration
 226    fibonacciConfig: FibonacciSphereConfig;
 227    setFibonacciConfig: (config: Partial<FibonacciSphereConfig>) => void;
 228    resetFibonacciConfig: () => void;
 229    
 230    // Camera state management
 231    camera: {
 232      position: [number, number, number];
 233      target: [number, number, number];
 234      isTransitioning: boolean;
 235      transitionDuration: number;
 236    };
 237    setCameraPosition: (position: [number, number, number]) => void;
 238    setCameraTarget: (target: [number, number, number]) => void;
 239    setCameraTransition: (isTransitioning: boolean, duration?: number) => void;
 240    
 241    // Layout transition state
 242    layoutTransition: {
 243      isTransitioning: boolean;
 244      progress: number;
 245      previousLayout: 'constellation' | 'creation' | 'search' | 'liminal-web' | 'edit' | 'edit-search' | 'copilot' | null;
 246    };
 247    setLayoutTransition: (isTransitioning: boolean, progress?: number, previousLayout?: 'constellation' | 'creation' | 'search' | 'liminal-web' | 'edit' | 'edit-search' | 'copilot' | null) => void;
 248    
 249    // Debug wireframe sphere toggle
 250    debugWireframeSphere: boolean;
 251    setDebugWireframeSphere: (visible: boolean) => void;
 252    
 253    // Debug intersection point toggle
 254    debugIntersectionPoint: boolean;
 255    setDebugIntersectionPoint: (visible: boolean) => void;
 256    
 257    // Debug flying camera controls toggle
 258    debugFlyingControls: boolean;
 259    setDebugFlyingControls: (enabled: boolean) => void;
 260    
 261    // Mock data configuration
 262    mockDataConfig: MockDataConfig;
 263    setMockDataConfig: (config: MockDataConfig) => void;
 264    
 265    // Persistent mock relationship data
 266    mockRelationshipData: Map<string, string[]> | null;
 267    generateMockRelationships: () => void;
 268    clearMockRelationships: () => void;
 269    
 270    // Drag state management (prevents hover interference during sphere rotation)
 271    isDragging: boolean;
 272    setIsDragging: (dragging: boolean) => void;
 273    
 274    // Creation state management
 275    creationState: CreationState;
 276    startCreation: (position: [number, number, number]) => void;
 277    startCreationWithData: (position: [number, number, number], initialData?: Partial<ProtoNode>) => void;
 278    updateProtoNode: (updates: Partial<ProtoNode>) => void;
 279    setValidationErrors: (errors: ValidationErrors) => void;
 280    completeCreation: () => void;
 281    cancelCreation: () => void;
 282  
 283    // Edit mode state management
 284    editMode: EditModeState;
 285    startEditMode: (node: DreamNode) => void;
 286    exitEditMode: () => void;
 287    updateEditingNodeMetadata: (updates: Partial<DreamNode>) => void;
 288    setEditModeNewDreamTalkFile: (file: globalThis.File | undefined) => void;
 289    setEditModeSearchResults: (results: DreamNode[]) => void;
 290    setEditModeSearchActive: (active: boolean) => void;
 291    togglePendingRelationship: (nodeId: string) => void;
 292    savePendingRelationships: () => void;
 293    setEditModeValidationErrors: (errors: EditModeValidationErrors) => void;
 294  
 295    // Copilot mode state management
 296    copilotMode: CopilotModeState;
 297    startCopilotMode: (conversationPartner: DreamNode) => void;
 298    exitCopilotMode: () => void;
 299    setShowSearchResults: (show: boolean) => void;
 300    freezeSearchResults: () => void;
 301    addSharedNode: (nodeId: string) => void;
 302  
 303    // Navigation history management
 304    navigationHistory: NavigationHistoryState;
 305    isRestoringFromHistory: boolean;
 306    setRestoringFromHistory: (restoring: boolean) => void;
 307    addHistoryEntry: (nodeId: string | null, layout: 'constellation' | 'liminal-web') => void;
 308    getHistoryEntryForUndo: () => NavigationHistoryEntry | null;
 309    getHistoryEntryForRedo: () => NavigationHistoryEntry | null;
 310    performUndo: () => boolean;
 311    performRedo: () => boolean;
 312    clearNavigationHistory: () => void;
 313    restoreVisualState: (entry: NavigationHistoryEntry) => void;
 314    
 315    // DreamNode flip animation state
 316    flipState: {
 317      flippedNodeId: string | null;
 318      flipStates: Map<string, FlipState>;
 319    };
 320    setFlippedNode: (nodeId: string | null) => void;
 321    startFlipAnimation: (nodeId: string, direction: 'front-to-back' | 'back-to-front') => void;
 322    completeFlipAnimation: (nodeId: string) => void;
 323    resetAllFlips: () => void;
 324    getNodeFlipState: (nodeId: string) => FlipState | null;
 325  
 326    // DreamSong relationship graph state
 327    constellationData: {
 328      relationshipGraph: DreamSongRelationshipGraph | null;
 329      lastScanTimestamp: number | null;
 330      isScanning: boolean;
 331      positions: Map<string, [number, number, number]> | null;
 332      lastLayoutTimestamp: number | null;
 333      // Lightweight node metadata for instant startup rendering
 334      nodeMetadata: Map<string, { name: string; type: string; uuid: string }> | null;
 335    };
 336    setRelationshipGraph: (graph: DreamSongRelationshipGraph | null) => void;
 337    setConstellationScanning: (scanning: boolean) => void;
 338    setConstellationPositions: (positions: Map<string, [number, number, number]> | null) => void;
 339    setNodeMetadata: (metadata: Map<string, { name: string; type: string; uuid: string }> | null) => void;
 340    clearConstellationData: () => void;
 341  
 342    // Radial button UI state (option-key triggered)
 343    radialButtonUI: {
 344      isActive: boolean;
 345      buttonCount: number;
 346      optionKeyPressed: boolean; // Track actual hardware key state
 347    };
 348    setRadialButtonUIActive: (active: boolean) => void;
 349    setRadialButtonCount: (count: number) => void;
 350    setOptionKeyPressed: (pressed: boolean) => void;
 351  
 352    // Update status for DreamNodes (non-persisted)
 353    updateStatus: Map<string, import('../services/git-service').FetchResult>;
 354    setNodeUpdateStatus: (nodeId: string, result: import('../services/git-service').FetchResult) => void;
 355    clearNodeUpdateStatus: (nodeId: string) => void;
 356    getNodeUpdateStatus: (nodeId: string) => import('../services/git-service').FetchResult | null;
 357  }
 358  
 359  // Helper to convert Map to serializable format for persistence
 360  const mapToArray = <K, V>(map: Map<K, V>): [K, V][] => Array.from(map.entries());
 361  const arrayToMap = <K, V>(array: [K, V][]): Map<K, V> => new Map(array);
 362  
 363  export const useInterBrainStore = create<InterBrainState>()(
 364    persist(
 365      (set, get) => ({
 366    // Initial state
 367    dataMode: 'real' as const, // Start in real mode by default
 368    realNodes: new Map<string, RealNodeData>(),
 369    
 370    // Initialize Ollama config slice
 371    ...createOllamaConfigSlice(set, get, {} as never),
 372    selectedNode: null,
 373    selectedNodeDreamSongData: null,
 374  
 375    // DreamSong cache for service layer
 376    dreamSongCache: new Map<string, DreamSongCacheEntry>(),
 377  
 378    creatorMode: {
 379      isActive: false,
 380      nodeId: null
 381    },
 382    searchResults: [],
 383    searchInterface: {
 384      isActive: false,
 385      isSaving: false,
 386      currentQuery: '',
 387      lastQuery: ''
 388    },
 389    spatialLayout: 'constellation',
 390    fibonacciConfig: DEFAULT_FIBONACCI_CONFIG,
 391    
 392    // Camera initial state
 393    camera: {
 394      position: [0, 0, 0], // Camera at origin for proper Dynamic View Scaling
 395      target: [0, 0, 0],   // Looking at origin
 396      isTransitioning: false,
 397      transitionDuration: 1000, // 1 second default
 398    },
 399    
 400    // Layout transition initial state
 401    layoutTransition: {
 402      isTransitioning: false,
 403      progress: 0,
 404      previousLayout: null,
 405    },
 406    
 407    // Debug wireframe sphere initial state (on by default for development)
 408    debugWireframeSphere: false,
 409    
 410    // Debug intersection point initial state (on by default for development)
 411    debugIntersectionPoint: false,
 412    
 413    // Debug flying camera controls initial state (off by default)
 414    debugFlyingControls: false,
 415    
 416    // Mock data configuration initial state (single node for testing)
 417    mockDataConfig: 'fibonacci-100',
 418    
 419    // Persistent mock relationship data initial state
 420    mockRelationshipData: null,
 421    
 422    // Drag state initial state (not dragging)
 423    isDragging: false,
 424    
 425    // Creation state initial state (not creating)
 426    creationState: {
 427      isCreating: false,
 428      protoNode: null,
 429      validationErrors: {}
 430    },
 431    
 432    // Edit mode initial state (not editing)
 433    editMode: {
 434      isActive: false,
 435      editingNode: null,
 436      originalRelationships: [],
 437      pendingRelationships: [],
 438      searchResults: [],
 439      validationErrors: {},
 440      isSearchingRelationships: false
 441    },
 442  
 443    // Copilot mode initial state (not active)
 444    copilotMode: {
 445      isActive: false,
 446      conversationPartner: null,
 447      transcriptionFilePath: null,
 448      showSearchResults: false,
 449      frozenSearchResults: [],
 450      sharedNodeIds: []
 451    },
 452  
 453    // Navigation history initial state (with initial constellation state)
 454    navigationHistory: {
 455      history: [{
 456        nodeId: null,
 457        layout: 'constellation',
 458        timestamp: Date.now(),
 459        flipState: null,
 460        scrollPosition: null
 461      }],
 462      currentIndex: 0, // Start at the initial constellation state
 463      maxHistorySize: 150 // High limit for ultra-lightweight entries
 464    },
 465    
 466    // Flag to disable history tracking during undo/redo operations
 467    isRestoringFromHistory: false,
 468    
 469    // DreamNode flip animation initial state
 470    flipState: {
 471      flippedNodeId: null,
 472      flipStates: new Map<string, FlipState>()
 473    },
 474  
 475    // Constellation relationship graph initial state
 476    constellationData: {
 477      relationshipGraph: null,
 478      lastScanTimestamp: null,
 479      isScanning: false,
 480      positions: null,
 481      lastLayoutTimestamp: null,
 482      nodeMetadata: null
 483    },
 484  
 485    radialButtonUI: {
 486      isActive: false,
 487      buttonCount: 6,
 488      optionKeyPressed: false
 489    },
 490  
 491    // Actions
 492    setDataMode: (mode) => set({ dataMode: mode }),
 493    setRealNodes: (nodes) => set({ realNodes: nodes }),
 494    updateRealNode: (id, data) => set(state => {
 495      const newMap = new Map(state.realNodes);
 496      newMap.set(id, data);
 497      return { realNodes: newMap };
 498    }),
 499    batchUpdateNodePositions: (positions) => set(state => {
 500      const newMap = new Map(state.realNodes);
 501      for (const [nodeId, position] of positions) {
 502        const nodeData = newMap.get(nodeId);
 503        if (nodeData) {
 504          newMap.set(nodeId, {
 505            ...nodeData,
 506            node: { ...nodeData.node, position }
 507          });
 508        }
 509      }
 510      return { realNodes: newMap };
 511    }),
 512    deleteRealNode: (id) => set(state => {
 513      const newMap = new Map(state.realNodes);
 514      newMap.delete(id);
 515      return { realNodes: newMap };
 516    }),
 517    
 518    // Note: Vector data and Ollama config actions provided by OllamaConfigSlice
 519    
 520    setSelectedNode: (node) => set(state => {
 521      const previousNode = state.selectedNode;
 522      const currentLayout = state.spatialLayout;
 523  
 524      // Trigger lazy media loading for node and 2-degree neighborhood
 525      if (node) {
 526        // Import and trigger media loading asynchronously (non-blocking)
 527        import('../services/media-loading-service').then(({ getMediaLoadingService }) => {
 528          try {
 529            const mediaLoadingService = getMediaLoadingService();
 530            mediaLoadingService.loadNodeWithNeighborhood(node.id);
 531          } catch (error) {
 532            console.warn('[Store] MediaLoadingService not initialized:', error);
 533          }
 534        }).catch(error => {
 535          console.error('[Store] Failed to load media service:', error);
 536        });
 537      }
 538  
 539      // Detect meaningful node selection changes for history tracking
 540      const isMeaningfulChange = (
 541        // Within Liminal Web: different node selected
 542        currentLayout === 'liminal-web' &&
 543        previousNode &&
 544        node &&
 545        previousNode.id !== node.id
 546      );
 547  
 548      // Record history entry for meaningful changes (but not during undo/redo operations)
 549      if (isMeaningfulChange && !state.isRestoringFromHistory) {
 550        // Create new entry
 551        const newEntry: NavigationHistoryEntry = {
 552          nodeId: node.id,
 553          layout: 'liminal-web',
 554          timestamp: Date.now(),
 555          flipState: state.flipState.flipStates.get(node.id) || null,
 556          scrollPosition: getDreamSongScrollPosition(node.id) // We'll implement this helper
 557        };
 558        
 559        // Add to history (reuse existing addHistoryEntry logic)
 560        const { history, currentIndex, maxHistorySize } = state.navigationHistory;
 561        
 562        // Check if this entry is a duplicate of the current entry (prevent undo/redo loops)
 563        const currentEntry = history[currentIndex];
 564        const isDuplicate = currentEntry && 
 565          currentEntry.nodeId === newEntry.nodeId && 
 566          currentEntry.layout === newEntry.layout;
 567        
 568        if (isDuplicate) {
 569          // Don't add duplicate entry - just update selected node
 570          return { selectedNode: node };
 571        }
 572        
 573        // If we're not at the end of history, clear everything after current position
 574        const newHistory = currentIndex >= 0 
 575          ? [...history.slice(0, currentIndex + 1), newEntry]
 576          : [newEntry];
 577        
 578        // Ensure history doesn't exceed max size
 579        const trimmedHistory = newHistory.length > maxHistorySize
 580          ? newHistory.slice(-maxHistorySize)
 581          : newHistory;
 582        
 583        return {
 584          selectedNode: node,
 585          navigationHistory: {
 586            ...state.navigationHistory,
 587            history: trimmedHistory,
 588            currentIndex: trimmedHistory.length - 1
 589          }
 590        };
 591      }
 592      
 593      // Non-meaningful change - just update selected node
 594      return { selectedNode: node };
 595    }),
 596    
 597    setSelectedNodeDreamSongData: (data) => set({ selectedNodeDreamSongData: data }),
 598  
 599    // DreamSong cache methods for service layer
 600    getCachedDreamSong: (nodeId: string, structureHash: string) => {
 601      const cacheKey = `${nodeId}-${structureHash}`;
 602      return get().dreamSongCache.get(cacheKey) || null;
 603    },
 604  
 605    setCachedDreamSong: (nodeId: string, structureHash: string, data: DreamSongData) => {
 606      const cacheKey = `${nodeId}-${structureHash}`;
 607      const entry: DreamSongCacheEntry = {
 608        data,
 609        timestamp: Date.now(),
 610        structureHash
 611      };
 612      set((state) => {
 613        const newCache = new Map(state.dreamSongCache);
 614        newCache.set(cacheKey, entry);
 615        return { dreamSongCache: newCache };
 616      });
 617    },
 618  
 619    setCreatorMode: (active, nodeId = null) => set({ 
 620      creatorMode: { isActive: active, nodeId: nodeId } 
 621    }),
 622    setSearchResults: (results) => set({ searchResults: results }),
 623    setSearchActive: (active) => set(state => ({
 624      searchInterface: {
 625        ...state.searchInterface,
 626        isActive: active,
 627        // Clear query when deactivating for fresh start on reentry
 628        currentQuery: active ? state.searchInterface.currentQuery : '',
 629        lastQuery: active ? state.searchInterface.lastQuery : ''
 630      },
 631      // Also clear search results when deactivating
 632      searchResults: active ? state.searchResults : []
 633    })),
 634    setSearchQuery: (query) => set(state => ({
 635      searchInterface: {
 636        ...state.searchInterface,
 637        currentQuery: query
 638      }
 639    })),
 640    setSearchSaving: (saving) => set(state => ({
 641      searchInterface: {
 642        ...state.searchInterface,
 643        isSaving: saving
 644      }
 645    })),
 646    setSpatialLayout: (layout) => set(state => {
 647      const previousLayout = state.spatialLayout;
 648      const selectedNode = state.selectedNode;
 649      
 650      // Only log actual changes, not redundant calls
 651      if (previousLayout !== layout) {
 652      }
 653      
 654      // Detect meaningful layout changes for history tracking
 655      const isMeaningfulChange = (
 656        // Constellation → Liminal Web (with selected node)
 657        (previousLayout === 'constellation' && layout === 'liminal-web' && selectedNode) ||
 658        // Liminal Web → Constellation  
 659        (previousLayout === 'liminal-web' && layout === 'constellation')
 660        // Note: Within Liminal Web changes are handled by setSelectedNode
 661      );
 662      
 663      // Record history entry for meaningful changes (but not during undo/redo operations)
 664      if (isMeaningfulChange && !state.isRestoringFromHistory) {
 665        // Create new entry (inside this block, layout can only be 'constellation' or 'liminal-web')
 666        const newEntry: NavigationHistoryEntry = {
 667          nodeId: layout === 'liminal-web' ? selectedNode?.id || null : null,
 668          layout: layout, // Already narrowed to valid types by isMeaningfulChange condition
 669          timestamp: Date.now(),
 670          flipState: (layout === 'liminal-web' && selectedNode) ? 
 671            state.flipState.flipStates.get(selectedNode.id) || null : null,
 672          scrollPosition: (layout === 'liminal-web' && selectedNode) ? 
 673            getDreamSongScrollPosition(selectedNode.id) : null
 674        };
 675        
 676        // Add to history (reuse existing addHistoryEntry logic)
 677        const { history, currentIndex, maxHistorySize } = state.navigationHistory;
 678        
 679        // Check if this entry is a duplicate of the current entry (prevent undo/redo loops)
 680        const currentEntry = history[currentIndex];
 681        const isDuplicate = currentEntry && 
 682          currentEntry.nodeId === newEntry.nodeId && 
 683          currentEntry.layout === newEntry.layout;
 684        
 685        if (isDuplicate) {
 686          // Don't add duplicate entry - just update layout without history changes
 687          return {
 688            spatialLayout: layout,
 689            layoutTransition: {
 690              ...state.layoutTransition,
 691              previousLayout: state.spatialLayout,
 692            }
 693          };
 694        }
 695        
 696        // If we're not at the end of history, clear everything after current position
 697        const newHistory = currentIndex >= 0 
 698          ? [...history.slice(0, currentIndex + 1), newEntry]
 699          : [newEntry];
 700        
 701        // Ensure history doesn't exceed max size
 702        const trimmedHistory = newHistory.length > maxHistorySize
 703          ? newHistory.slice(-maxHistorySize)
 704          : newHistory;
 705        
 706        return {
 707          spatialLayout: layout,
 708          layoutTransition: {
 709            ...state.layoutTransition,
 710            previousLayout: state.spatialLayout,
 711          },
 712          navigationHistory: {
 713            ...state.navigationHistory,
 714            history: trimmedHistory,
 715            currentIndex: trimmedHistory.length - 1
 716          }
 717        };
 718      }
 719      
 720      // Non-meaningful change - just update layout
 721      return {
 722        spatialLayout: layout,
 723        layoutTransition: {
 724          ...state.layoutTransition,
 725          previousLayout: state.spatialLayout,
 726        }
 727      };
 728    }),
 729    
 730    // Fibonacci sphere configuration actions
 731    setFibonacciConfig: (config) => set(state => ({
 732      fibonacciConfig: { ...state.fibonacciConfig, ...config }
 733    })),
 734    resetFibonacciConfig: () => set({ fibonacciConfig: DEFAULT_FIBONACCI_CONFIG }),
 735    
 736    // Camera actions
 737    setCameraPosition: (position) => set(state => ({ 
 738      camera: { ...state.camera, position } 
 739    })),
 740    setCameraTarget: (target) => set(state => ({ 
 741      camera: { ...state.camera, target } 
 742    })),
 743    setCameraTransition: (isTransitioning, duration = 1000) => set(state => ({ 
 744      camera: { ...state.camera, isTransitioning, transitionDuration: duration } 
 745    })),
 746    
 747    // Layout transition actions
 748    setLayoutTransition: (isTransitioning, progress = 0, previousLayout = null) => set(state => ({ 
 749      layoutTransition: { 
 750        isTransitioning, 
 751        progress, 
 752        previousLayout: previousLayout || state.layoutTransition.previousLayout 
 753      } 
 754    })),
 755    
 756    // Debug wireframe sphere actions
 757    setDebugWireframeSphere: (visible) => set({ debugWireframeSphere: visible }),
 758    
 759    // Debug intersection point actions
 760    setDebugIntersectionPoint: (visible) => set({ debugIntersectionPoint: visible }),
 761    
 762    // Debug flying camera controls actions
 763    setDebugFlyingControls: (enabled) => set({ debugFlyingControls: enabled }),
 764    
 765    // Mock data configuration actions
 766    setMockDataConfig: (config) => set({ mockDataConfig: config }),
 767    
 768    // Mock relationship data actions
 769    generateMockRelationships: () => set(state => {
 770      const { mockDataConfig } = state;
 771      const nodeCount = mockDataConfig === 'single-node' ? 1 : 
 772                       mockDataConfig === 'fibonacci-12' ? 12 :
 773                       mockDataConfig === 'fibonacci-50' ? 50 : 100;
 774      
 775      const relationships = new Map<string, string[]>();
 776      
 777      // First pass: Initialize all nodes in the map
 778      for (let i = 0; i < nodeCount; i++) {
 779        const nodeType = i % 3 !== 0 ? 'dream' : 'dreamer';
 780        const nodeId = `mock-${nodeType}-${i}`;
 781        relationships.set(nodeId, []);
 782      }
 783      
 784      // Second pass: Generate bidirectional relationships between Dreams and Dreamers
 785      for (let i = 0; i < nodeCount; i++) {
 786        const sourceType = i % 3 !== 0 ? 'dream' : 'dreamer';
 787        const sourceId = `mock-${sourceType}-${i}`;
 788        
 789        // Use deterministic pattern for consistent relationships with more variety
 790        const stepSizes = [1, 2, 3, 5, 7, 11, 13, 17, 19];
 791        
 792        // Create more diversity in connection counts based on node index
 793        const baseConnections = 2;
 794        const variabilityFactor = ((i * 13) % 17) / 17; // 0 to 1, varies by node
 795        const maxConnections = Math.min(
 796          Math.floor(nodeCount * 0.8), // Up to 80% of opposite-type nodes
 797          baseConnections + Math.floor(variabilityFactor * Math.min(25, nodeCount - baseConnections))
 798        );
 799        
 800        for (let j = 0; j < Math.min(stepSizes.length, maxConnections); j++) {
 801          const step = stepSizes[j];
 802          const targetIndex = (i + step) % nodeCount;
 803          const targetType = targetIndex % 3 !== 0 ? 'dream' : 'dreamer';
 804          
 805          // Only connect Dreams to Dreamers and vice versa
 806          if (sourceType !== targetType) {
 807            const targetId = `mock-${targetType}-${targetIndex}`;
 808            
 809            // Add forward connection if not already present
 810            const sourceConnections = relationships.get(sourceId)!;
 811            if (!sourceConnections.includes(targetId)) {
 812              sourceConnections.push(targetId);
 813            }
 814            
 815            // Add reverse connection if not already present
 816            const targetConnections = relationships.get(targetId)!;
 817            if (!targetConnections.includes(sourceId)) {
 818              targetConnections.push(sourceId);
 819            }
 820          }
 821        }
 822        
 823        // Ensure at least one connection if possible
 824        const sourceConnections = relationships.get(sourceId)!;
 825        if (sourceConnections.length === 0 && nodeCount > 1) {
 826          for (let offset = 1; offset < nodeCount; offset++) {
 827            const targetIndex = (i + offset) % nodeCount;
 828            const targetType = targetIndex % 3 !== 0 ? 'dream' : 'dreamer';
 829            
 830            if (sourceType !== targetType) {
 831              const targetId = `mock-${targetType}-${targetIndex}`;
 832              
 833              // Add forward connection
 834              sourceConnections.push(targetId);
 835              
 836              // Add reverse connection
 837              const targetConnections = relationships.get(targetId)!;
 838              if (!targetConnections.includes(sourceId)) {
 839                targetConnections.push(sourceId);
 840              }
 841              break;
 842            }
 843          }
 844        }
 845      }
 846      
 847      // Verify bidirectionality (basic check)
 848      relationships.forEach((connections, sourceId) => {
 849        connections.forEach(targetId => {
 850          const targetConnections = relationships.get(targetId);
 851          if (!targetConnections || !targetConnections.includes(sourceId)) {
 852            // Skip tracking errors for now - just ensure basic structure
 853          }
 854        });
 855      });
 856      
 857      return { mockRelationshipData: relationships };
 858    }),
 859    
 860    clearMockRelationships: () => set({ mockRelationshipData: null }),
 861    
 862    // Drag state actions
 863    setIsDragging: (dragging) => set({ isDragging: dragging }),
 864    
 865    // Creation state actions
 866    startCreation: (position) => set((_state) => {
 867      return {
 868        spatialLayout: 'creation',
 869        creationState: {
 870          isCreating: true,
 871          protoNode: {
 872            title: '',
 873            type: 'dream', // Default to dream type
 874            position,
 875            dreamTalkFile: undefined
 876          },
 877          validationErrors: {}
 878        }
 879      };
 880    }),
 881    
 882    startCreationWithData: (position, initialData) => set((_state) => {
 883      return {
 884        spatialLayout: 'creation',
 885        creationState: {
 886          isCreating: true,
 887          protoNode: {
 888            title: initialData?.title || '',
 889            type: initialData?.type || 'dream',
 890            position,
 891            dreamTalkFile: initialData?.dreamTalkFile || undefined,
 892            additionalFiles: initialData?.additionalFiles || undefined
 893          },
 894          validationErrors: {}
 895        }
 896      };
 897    }),
 898    
 899    updateProtoNode: (updates) => set(state => ({
 900      creationState: {
 901        ...state.creationState,
 902        protoNode: state.creationState.protoNode 
 903          ? { ...state.creationState.protoNode, ...updates }
 904          : null
 905      }
 906    })),
 907    
 908    setValidationErrors: (errors) => set(state => ({
 909      creationState: {
 910        ...state.creationState,
 911        validationErrors: errors
 912      }
 913    })),
 914    
 915    completeCreation: () => set((_state) => {
 916      return {
 917        spatialLayout: 'constellation',
 918        creationState: {
 919          isCreating: false,
 920          protoNode: null,
 921          validationErrors: {}
 922        }
 923      };
 924    }),
 925    
 926    cancelCreation: () => set((_state) => {
 927      return {
 928        spatialLayout: 'constellation',
 929        creationState: {
 930          isCreating: false,
 931          protoNode: null,
 932          validationErrors: {}
 933        }
 934      };
 935    }),
 936  
 937    // Edit mode actions
 938    startEditMode: (node) => set((_state) => {
 939      return {
 940        editMode: {
 941          isActive: true,
 942          editingNode: { ...node }, // Create a copy to avoid mutations
 943          originalRelationships: [...node.liminalWebConnections], // Store original relationships
 944          pendingRelationships: [...node.liminalWebConnections], // Show existing relationships with glow
 945          searchResults: [],
 946          validationErrors: {},
 947          isSearchingRelationships: false
 948        },
 949        // Also set the spatial layout to 'edit' mode
 950        spatialLayout: 'edit' as const
 951      };
 952    }),
 953  
 954    exitEditMode: () => set((_state) => {
 955      // Note: We don't change the layout here - the calling code should handle that
 956      // This allows for proper transitions (edit → liminal-web, edit-search → edit, etc.)
 957      
 958      return {
 959        editMode: {
 960          isActive: false,
 961          editingNode: null,
 962          originalRelationships: [],
 963          pendingRelationships: [],
 964          searchResults: [],
 965          validationErrors: {},
 966          newDreamTalkFile: undefined,
 967          isSearchingRelationships: false
 968        }
 969      };
 970    }),
 971  
 972    updateEditingNodeMetadata: (updates) => set(state => ({
 973      editMode: {
 974        ...state.editMode,
 975        editingNode: state.editMode.editingNode
 976          ? { ...state.editMode.editingNode, ...updates }
 977          : null
 978      }
 979    })),
 980  
 981    setEditModeNewDreamTalkFile: (file) => set(state => ({
 982      editMode: {
 983        ...state.editMode,
 984        newDreamTalkFile: file
 985      }
 986    })),
 987  
 988    setEditModeSearchResults: (results) => set(state => ({
 989      editMode: {
 990        ...state.editMode,
 991        searchResults: results
 992      }
 993    })),
 994  
 995    setEditModeSearchActive: (active) => set(state => {
 996      const newLayout = active ? 'edit-search' as const : 'edit' as const;
 997  
 998      return {
 999        editMode: {
1000          ...state.editMode,
1001          isSearchingRelationships: active,
1002          // Clear search results when exiting edit-search mode to prevent persistence
1003          searchResults: active ? state.editMode.searchResults : []
1004        },
1005        // Update spatial layout based on search mode state
1006        spatialLayout: newLayout,
1007        // Also clear main search results when exiting edit-search to clean up spatial layout
1008        searchResults: active ? state.searchResults : []
1009      };
1010    }),
1011  
1012    togglePendingRelationship: (nodeId) => set(state => {
1013      const currentPending = state.editMode.pendingRelationships;
1014      const isAlreadyPending = currentPending.includes(nodeId);
1015      
1016      return {
1017        editMode: {
1018          ...state.editMode,
1019          pendingRelationships: isAlreadyPending
1020            ? currentPending.filter(id => id !== nodeId) // Remove if exists
1021            : [...currentPending, nodeId] // Add if doesn't exist
1022        }
1023      };
1024    }),
1025  
1026    savePendingRelationships: () => set(state => {
1027      if (!state.editMode.editingNode) return state;
1028  
1029      // Update the editing node with pending relationships
1030      const updatedNode = {
1031        ...state.editMode.editingNode,
1032        liminalWebConnections: [...state.editMode.pendingRelationships]
1033      };
1034  
1035      return {
1036        editMode: {
1037          ...state.editMode,
1038          editingNode: updatedNode,
1039          originalRelationships: [...state.editMode.pendingRelationships] // Update original to match
1040        }
1041      };
1042    }),
1043  
1044    setEditModeValidationErrors: (errors) => set(state => ({
1045      editMode: {
1046        ...state.editMode,
1047        validationErrors: errors
1048      }
1049    })),
1050  
1051    // Copilot mode actions
1052    startCopilotMode: (conversationPartner) => set((_state) => {
1053      // Hide ribbon for cleaner video call interface
1054      try {
1055        const app = (globalThis as any).app;
1056        if (app?.workspace?.leftRibbon) {
1057          app.workspace.leftRibbon.hide();
1058          console.log(`đŸŽ¯ [Copilot-Entry] Hidden ribbon for cleaner interface`);
1059        }
1060      } catch (error) {
1061        console.warn('Failed to hide ribbon:', error);
1062      }
1063  
1064      return {
1065        spatialLayout: 'copilot',
1066        copilotMode: {
1067          isActive: true,
1068          conversationPartner: { ...conversationPartner }, // Create a copy
1069          transcriptionFilePath: null,
1070          showSearchResults: false,
1071          frozenSearchResults: [],
1072          sharedNodeIds: []
1073        }
1074      };
1075    }),
1076  
1077    exitCopilotMode: () => set((state) => {
1078      // PROCESS SHARED NODES BEFORE CLEARING STATE
1079      const { conversationPartner, sharedNodeIds } = state.copilotMode;
1080  
1081      if (conversationPartner && sharedNodeIds.length > 0) {
1082        console.log(`🔗 [Copilot-Exit] Processing ${sharedNodeIds.length} shared nodes for "${conversationPartner.name}"`);
1083        console.log(`🔗 [Copilot-Exit] Shared node IDs: ${sharedNodeIds.join(', ')}`);
1084  
1085        // Filter out nodes that are already related to avoid duplicates
1086        const newRelationships = sharedNodeIds.filter(id => !conversationPartner.liminalWebConnections.includes(id));
1087  
1088        if (newRelationships.length > 0) {
1089          // Create updated conversation partner with new relationships
1090          const updatedPartner = {
1091            ...conversationPartner,
1092            liminalWebConnections: [...conversationPartner.liminalWebConnections, ...newRelationships]
1093          };
1094  
1095          console.log(`✅ [Copilot-Exit] Adding ${newRelationships.length} new relationships: ${newRelationships.join(', ')}`);
1096          console.log(`✅ [Copilot-Exit] "${conversationPartner.name}" now has ${updatedPartner.liminalWebConnections.length} total relationships`);
1097  
1098          // Update the conversation partner node in store
1099          const existingNodeData = state.realNodes.get(conversationPartner.id);
1100          if (existingNodeData) {
1101            state.realNodes.set(conversationPartner.id, {
1102              ...existingNodeData,
1103              node: updatedPartner
1104            });
1105          }
1106  
1107          // Also update selectedNode if it matches the conversation partner
1108          if (state.selectedNode?.id === conversationPartner.id) {
1109            state.selectedNode = updatedPartner;
1110          }
1111  
1112          // Update bidirectional relationships in store for immediate UI feedback
1113          for (const sharedNodeId of newRelationships) {
1114            const sharedNodeData = state.realNodes.get(sharedNodeId);
1115            if (sharedNodeData) {
1116              const updatedSharedNode = {
1117                ...sharedNodeData.node,
1118                liminalWebConnections: [...sharedNodeData.node.liminalWebConnections, conversationPartner.id]
1119              };
1120              state.realNodes.set(sharedNodeId, {
1121                ...sharedNodeData,
1122                node: updatedSharedNode
1123              });
1124              console.log(`✅ [Copilot-Exit] Updated bidirectional relationship for shared node: ${updatedSharedNode.name}`);
1125            }
1126          }
1127        } else {
1128          console.log(`â„šī¸ [Copilot-Exit] No new relationships to add - all shared nodes were already related`);
1129        }
1130      }
1131  
1132      // Show ribbon again when exiting copilot mode
1133      try {
1134        const app = (globalThis as any).app;
1135        if (app?.workspace?.leftRibbon) {
1136          app.workspace.leftRibbon.show();
1137          console.log(`đŸŽ¯ [Copilot-Exit] Restored ribbon visibility`);
1138        }
1139      } catch (error) {
1140        console.warn('Failed to show ribbon:', error);
1141      }
1142  
1143      return {
1144        spatialLayout: 'liminal-web', // Return to liminal-web layout with updated relationships
1145        copilotMode: {
1146          isActive: false,
1147          conversationPartner: null,
1148          transcriptionFilePath: null,
1149          showSearchResults: false,
1150          frozenSearchResults: [],
1151          sharedNodeIds: []
1152        }
1153      };
1154    }),
1155  
1156    // Copilot show/hide actions
1157    setShowSearchResults: (show: boolean) => set((state) => ({
1158      copilotMode: {
1159        ...state.copilotMode,
1160        showSearchResults: show
1161      }
1162    })),
1163  
1164    freezeSearchResults: () => set((state) => ({
1165      copilotMode: {
1166        ...state.copilotMode,
1167        frozenSearchResults: [...state.searchResults] // Capture current search results
1168      }
1169    })),
1170  
1171    addSharedNode: (nodeId: string) => set((state) => ({
1172      copilotMode: {
1173        ...state.copilotMode,
1174        sharedNodeIds: [...state.copilotMode.sharedNodeIds, nodeId]
1175      }
1176    })),
1177  
1178    // Navigation history actions
1179    addHistoryEntry: (nodeId, layout) => set(state => {
1180      const { history, currentIndex, maxHistorySize } = state.navigationHistory;
1181      
1182      // Create new entry
1183      const newEntry: NavigationHistoryEntry = {
1184        nodeId,
1185        layout,
1186        timestamp: Date.now(),
1187        flipState: (nodeId && layout === 'liminal-web') ? 
1188          state.flipState.flipStates.get(nodeId) || null : null,
1189        scrollPosition: (nodeId && layout === 'liminal-web') ? 
1190          getDreamSongScrollPosition(nodeId) : null
1191      };
1192      
1193      // If we're not at the end of history (user has undone some actions),
1194      // clear everything after current position (standard undo/redo behavior)
1195      const newHistory = currentIndex >= 0 
1196        ? [...history.slice(0, currentIndex + 1), newEntry]
1197        : [newEntry];
1198      
1199      // Ensure history doesn't exceed max size (remove oldest entries)
1200      const trimmedHistory = newHistory.length > maxHistorySize
1201        ? newHistory.slice(-maxHistorySize)
1202        : newHistory;
1203      
1204      return {
1205        navigationHistory: {
1206          ...state.navigationHistory,
1207          history: trimmedHistory,
1208          currentIndex: trimmedHistory.length - 1
1209        }
1210      };
1211    }),
1212    
1213    getHistoryEntryForUndo: () => {
1214      // This function will be called from commands which have access to getState()
1215      return null; // Commands will implement the logic
1216    },
1217    
1218    getHistoryEntryForRedo: () => {
1219      // This function will be called from commands which have access to getState()
1220      return null; // Commands will implement the logic
1221    },
1222    
1223    performUndo: () => {
1224      let success = false;
1225      
1226      set(state => {
1227        const { currentIndex } = state.navigationHistory;
1228        
1229        if (currentIndex <= 0) {
1230          return state; // Nothing to undo
1231        }
1232        
1233        success = true;
1234        
1235        // Move to previous entry
1236        return {
1237          navigationHistory: {
1238            ...state.navigationHistory,
1239            currentIndex: currentIndex - 1
1240          }
1241        };
1242      });
1243      
1244      return success;
1245    },
1246    
1247    performRedo: () => {
1248      let success = false;
1249      
1250      set(state => {
1251        const { currentIndex, history } = state.navigationHistory;
1252        
1253        if (currentIndex >= history.length - 1) {
1254          return state; // Nothing to redo
1255        }
1256        
1257        success = true;
1258        
1259        // Move to next entry
1260        return {
1261          navigationHistory: {
1262            ...state.navigationHistory,
1263            currentIndex: currentIndex + 1
1264          }
1265        };
1266      });
1267      
1268      return success;
1269    },
1270    
1271    clearNavigationHistory: () => set(state => ({
1272      navigationHistory: {
1273        ...state.navigationHistory,
1274        history: [],
1275        currentIndex: -1
1276      }
1277    })),
1278    
1279    setRestoringFromHistory: (restoring) => set({ isRestoringFromHistory: restoring }),
1280    
1281    restoreVisualState: (entry) => set((state) => {
1282      const newState = { ...state };
1283  
1284      // Restore FlipState if present
1285      if (entry.nodeId && entry.flipState) {
1286        // Update the flip state for this node
1287        const updatedFlipStates = new Map(state.flipState.flipStates);
1288        updatedFlipStates.set(entry.nodeId, entry.flipState);
1289  
1290        newState.flipState = {
1291          ...state.flipState,
1292          flipStates: updatedFlipStates,
1293          flippedNodeId: entry.flipState.isFlipped ? entry.nodeId : state.flipState.flippedNodeId
1294        };
1295      }
1296  
1297      // Restore scroll position (async, but we don't wait for it)
1298      if (entry.nodeId && entry.scrollPosition !== null) {
1299        // Use setTimeout to ensure DOM has updated after state change
1300        if (typeof setTimeout !== 'undefined') {
1301           
1302          setTimeout(() => {
1303            restoreDreamSongScrollPosition(entry.nodeId!, entry.scrollPosition!);
1304          }, 100);
1305        } else {
1306          // Fallback for non-browser environments
1307          restoreDreamSongScrollPosition(entry.nodeId!, entry.scrollPosition!);
1308        }
1309      }
1310  
1311      return newState;
1312    }),
1313    
1314    // DreamNode flip animation actions
1315    setFlippedNode: (nodeId) => set((state) => {
1316      
1317      // Reset previous flipped node if different
1318      if (state.flipState.flippedNodeId && state.flipState.flippedNodeId !== nodeId) {
1319        const updatedFlipStates = new Map(state.flipState.flipStates);
1320        updatedFlipStates.delete(state.flipState.flippedNodeId);
1321        
1322        return {
1323          flipState: {
1324            flippedNodeId: nodeId,
1325            flipStates: updatedFlipStates
1326          }
1327        };
1328      }
1329      
1330      return {
1331        flipState: {
1332          ...state.flipState,
1333          flippedNodeId: nodeId
1334        }
1335      };
1336    }),
1337    
1338    startFlipAnimation: (nodeId, direction) => set((state) => {
1339      
1340      const updatedFlipStates = new Map(state.flipState.flipStates);
1341      
1342      const currentFlipState = updatedFlipStates.get(nodeId) || {
1343        isFlipped: false,
1344        isFlipping: false,
1345        flipDirection: 'front-to-back' as const,
1346        animationStartTime: 0
1347      };
1348      
1349      const newFlipState = {
1350        ...currentFlipState,
1351        isFlipping: true,
1352        flipDirection: direction,
1353        animationStartTime: globalThis.performance.now()
1354      };
1355      
1356      updatedFlipStates.set(nodeId, newFlipState);
1357  
1358      return {
1359        ...state,
1360        flipState: {
1361          ...state.flipState,
1362          flipStates: updatedFlipStates,
1363          flippedNodeId: nodeId
1364        }
1365      };
1366    }),
1367    
1368    completeFlipAnimation: (nodeId) => set((state) => {
1369      
1370      const updatedFlipStates = new Map(state.flipState.flipStates);
1371      const currentFlipState = updatedFlipStates.get(nodeId);
1372      
1373      if (currentFlipState) {
1374        const finalFlippedState = currentFlipState.flipDirection === 'front-to-back';
1375        const completedFlipState = {
1376          ...currentFlipState,
1377          isFlipped: finalFlippedState,
1378          isFlipping: false,
1379          animationStartTime: 0
1380        };
1381        
1382        updatedFlipStates.set(nodeId, completedFlipState);
1383      }
1384      
1385      return {
1386        flipState: {
1387          ...state.flipState,
1388          flipStates: updatedFlipStates
1389        }
1390      };
1391    }),
1392    
1393    resetAllFlips: () => set(() => ({
1394      flipState: {
1395        flippedNodeId: null,
1396        flipStates: new Map<string, FlipState>()
1397      }
1398    })),
1399    
1400    getNodeFlipState: (nodeId) => {
1401      const state = get();
1402      return state.flipState.flipStates.get(nodeId) || null;
1403    },
1404  
1405    // Constellation relationship graph actions
1406    setRelationshipGraph: (graph) => set((state) => ({
1407      constellationData: {
1408        ...state.constellationData,
1409        relationshipGraph: graph,
1410        lastScanTimestamp: graph ? Date.now() : null,
1411        isScanning: false
1412      }
1413    })),
1414  
1415    setConstellationScanning: (scanning) => set((state) => ({
1416      constellationData: {
1417        ...state.constellationData,
1418        isScanning: scanning
1419      }
1420    })),
1421  
1422    setConstellationPositions: (positions) => set((state) => ({
1423      constellationData: {
1424        ...state.constellationData,
1425        positions,
1426        lastLayoutTimestamp: positions ? Date.now() : null
1427      }
1428    })),
1429  
1430    setNodeMetadata: (metadata) => set((state) => ({
1431      constellationData: {
1432        ...state.constellationData,
1433        nodeMetadata: metadata
1434      }
1435    })),
1436  
1437    clearConstellationData: () => set(() => ({
1438      constellationData: {
1439        relationshipGraph: null,
1440        lastScanTimestamp: null,
1441        isScanning: false,
1442        positions: null,
1443        lastLayoutTimestamp: null,
1444        nodeMetadata: null
1445      }
1446    })),
1447  
1448    setRadialButtonUIActive: (active) => set((state) => ({
1449      radialButtonUI: {
1450        ...state.radialButtonUI,
1451        isActive: active
1452      }
1453    })),
1454  
1455    setRadialButtonCount: (count) => set((state) => ({
1456      radialButtonUI: {
1457        ...state.radialButtonUI,
1458        buttonCount: count
1459      }
1460    })),
1461  
1462    setOptionKeyPressed: (pressed) => set((state) => ({
1463      radialButtonUI: {
1464        ...state.radialButtonUI,
1465        optionKeyPressed: pressed
1466      }
1467    })),
1468  
1469    // Update status management (non-persisted)
1470    updateStatus: new Map(),
1471  
1472    setNodeUpdateStatus: (nodeId, result) => set((state) => {
1473      const newStatus = new Map(state.updateStatus);
1474      newStatus.set(nodeId, result);
1475      return { updateStatus: newStatus };
1476    }),
1477  
1478    clearNodeUpdateStatus: (nodeId) => set((state) => {
1479      const newStatus = new Map(state.updateStatus);
1480      newStatus.delete(nodeId);
1481      return { updateStatus: newStatus };
1482    }),
1483  
1484    getNodeUpdateStatus: (nodeId) => {
1485      return get().updateStatus.get(nodeId) || null;
1486    },
1487      }),
1488      {
1489        name: 'interbrain-storage', // Storage key
1490        // Only persist real nodes data, data mode, vector data, mock relationships, constellation data, and Ollama config
1491        partialize: (state) => ({
1492          dataMode: state.dataMode,
1493          // Don't persist realNodes at all - they reload from vault on app start
1494          // This avoids localStorage quota issues entirely
1495          realNodes: [],
1496          mockRelationshipData: state.mockRelationshipData ? mapToArray(state.mockRelationshipData) : null,
1497          constellationData: (state.constellationData.relationshipGraph || state.constellationData.positions || state.constellationData.nodeMetadata) ? {
1498            ...state.constellationData,
1499            relationshipGraph: state.constellationData.relationshipGraph ?
1500              serializeRelationshipGraph(state.constellationData.relationshipGraph) : null,
1501            positions: state.constellationData.positions ?
1502              mapToArray(state.constellationData.positions) : null,
1503            nodeMetadata: state.constellationData.nodeMetadata ?
1504              mapToArray(state.constellationData.nodeMetadata) : null
1505          } : null,
1506          ...extractOllamaPersistenceData(state),
1507        }),
1508        // Custom merge function to handle Map deserialization
1509        merge: (persisted: unknown, current) => {
1510          const persistedData = persisted as {
1511            dataMode: 'mock' | 'real';
1512            realNodes: [string, RealNodeData][];
1513            mockRelationshipData: [string, string[]][] | null;
1514            constellationData?: {
1515              relationshipGraph: SerializableDreamSongGraph | null;
1516              lastScanTimestamp: number | null;
1517              isScanning: boolean;
1518              positions: [string, [number, number, number]][] | null;
1519              lastLayoutTimestamp: number | null;
1520              nodeMetadata: [string, { name: string; type: string; uuid: string }][] | null;
1521            } | null;
1522            vectorData?: [string, VectorData][];
1523            ollamaConfig?: OllamaConfig;
1524          };
1525  
1526          // Restore constellation data if present
1527          let constellationData = {
1528            relationshipGraph: null as DreamSongRelationshipGraph | null,
1529            lastScanTimestamp: null as number | null,
1530            isScanning: false,
1531            positions: null as Map<string, [number, number, number]> | null,
1532            lastLayoutTimestamp: null as number | null,
1533            nodeMetadata: null as Map<string, { name: string; type: string; uuid: string }> | null
1534          };
1535  
1536          if (persistedData.constellationData) {
1537            try {
1538              constellationData = {
1539                relationshipGraph: persistedData.constellationData.relationshipGraph ?
1540                  deserializeRelationshipGraph(persistedData.constellationData.relationshipGraph) : null,
1541                lastScanTimestamp: persistedData.constellationData.lastScanTimestamp,
1542                isScanning: false,
1543                positions: persistedData.constellationData.positions ?
1544                  arrayToMap(persistedData.constellationData.positions) : null,
1545                lastLayoutTimestamp: persistedData.constellationData.lastLayoutTimestamp,
1546                nodeMetadata: persistedData.constellationData.nodeMetadata ?
1547                  arrayToMap(persistedData.constellationData.nodeMetadata) : null
1548              };
1549            } catch (error) {
1550              console.warn('Failed to deserialize constellation data:', error);
1551              // Keep default null state
1552            }
1553          }
1554  
1555          return {
1556            ...current,
1557            dataMode: persistedData.dataMode || 'mock',
1558            realNodes: persistedData.realNodes ? arrayToMap(persistedData.realNodes) : new Map(),
1559            mockRelationshipData: persistedData.mockRelationshipData ? arrayToMap(persistedData.mockRelationshipData) : null,
1560            constellationData,
1561            ...restoreOllamaPersistenceData(persistedData),
1562          };
1563        },
1564      }
1565    )
1566  );
1567  
1568  // DIAGNOSTIC: Log all store updates to identify re-render storm trigger
1569  if (typeof window !== 'undefined') {
1570    let updateCount = 0;
1571    useInterBrainStore.subscribe((state, prevState) => {
1572      updateCount++;
1573  
1574      // Diagnostic logging disabled
1575    });
1576  }