From c6ed0b77d8c45feb8e5f47eeac4689c4d99e1abf Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 24 Dec 2025 10:36:51 -0500 Subject: [PATCH] fix: register Calendar and Drawfast shapes in automerge store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added missing Calendar and Drawfast shapes to the automerge store schema registration to fix ValidationError when using these tools. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/automerge/CloudflareAdapter.ts | 201 +++----------------- src/automerge/useAutomergeStoreV2.ts | 8 + src/components/ObsidianVaultBrowser.tsx | 199 +++++--------------- src/utils/llmUtils.ts | 235 ++++++------------------ 4 files changed, 134 insertions(+), 509 deletions(-) diff --git a/src/automerge/CloudflareAdapter.ts b/src/automerge/CloudflareAdapter.ts index 209a109..df9bf8b 100644 --- a/src/automerge/CloudflareAdapter.ts +++ b/src/automerge/CloudflareAdapter.ts @@ -23,20 +23,16 @@ export class CloudflareAdapter { async getHandle(roomId: string): Promise> { if (!this.handles.has(roomId)) { - console.log(`Creating new Automerge handle for room ${roomId}`) const handle = this.repo.create() - + // Initialize with default store if this is a new document handle.change((doc) => { if (!doc.store) { - console.log("Initializing new document with default store") init(doc) } }) this.handles.set(roomId, handle) - } else { - console.log(`Reusing existing Automerge handle for room ${roomId}`) } return this.handles.get(roomId)! @@ -72,13 +68,11 @@ export class CloudflareAdapter { async saveToCloudflare(roomId: string): Promise { const handle = this.handles.get(roomId) if (!handle) { - console.log(`No handle found for room ${roomId}`) return } const doc = handle.doc() if (!doc) { - console.log(`No document found for room ${roomId}`) return } @@ -114,7 +108,6 @@ export class CloudflareAdapter { async loadFromCloudflare(roomId: string): Promise { try { - // Add retry logic for connection issues let response: Response; let retries = 3; @@ -131,7 +124,7 @@ export class CloudflareAdapter { } } } - + if (!response!.ok) { if (response!.status === 404) { return null // Room doesn't exist yet @@ -141,12 +134,7 @@ export class CloudflareAdapter { } const doc = await response!.json() as TLStoreSnapshot - console.log(`Successfully loaded document from Cloudflare for room ${roomId}:`, { - hasStore: !!doc.store, - storeKeys: doc.store ? Object.keys(doc.store).length : 0 - }) - - + // Initialize the last persisted state with the loaded document if (doc) { const docHash = this.generateDocHash(doc) @@ -202,7 +190,6 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { private setConnectionState(state: ConnectionState): void { if (this._connectionState !== state) { - console.log(`๐Ÿ”Œ Connection state: ${this._connectionState} โ†’ ${state}`) this._connectionState = state this.connectionStateListeners.forEach(listener => listener(state)) } @@ -237,7 +224,6 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { // Set up network online/offline listeners this.networkOnlineHandler = () => { - console.log('๐ŸŒ Network: online') this._isNetworkOnline = true // Trigger reconnect if we were disconnected if (this._connectionState === 'disconnected' && this.peerId) { @@ -246,7 +232,6 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { } } this.networkOfflineHandler = () => { - console.log('๐ŸŒ Network: offline') this._isNetworkOnline = false if (this._connectionState === 'connected') { this.setConnectionState('disconnected') @@ -273,12 +258,10 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { * @param documentId The Automerge document ID to use for incoming messages */ setDocumentId(documentId: string): void { - console.log('๐Ÿ“‹ CloudflareAdapter: Setting documentId:', documentId) this.currentDocumentId = documentId // Process any buffered binary messages now that we have a documentId if (this.pendingBinaryMessages.length > 0) { - console.log(`๐Ÿ“ฆ CloudflareAdapter: Processing ${this.pendingBinaryMessages.length} buffered binary messages`) const bufferedMessages = this.pendingBinaryMessages this.pendingBinaryMessages = [] @@ -290,7 +273,6 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { targetId: this.peerId || ('unknown' as PeerId), documentId: this.currentDocumentId as any } - console.log('๐Ÿ“ฅ CloudflareAdapter: Emitting buffered sync message with documentId:', this.currentDocumentId, 'size:', binaryData.byteLength) this.emit('message', message) } } @@ -305,7 +287,6 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { connect(peerId: PeerId, peerMetadata?: PeerMetadata): void { if (this.isConnecting) { - console.log('๐Ÿ”Œ CloudflareAdapter: Connection already in progress, skipping') return } @@ -329,33 +310,27 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { const wsUrl = `${protocol}${baseUrl}/connect/${this.roomId}?sessionId=${sessionId}` this.isConnecting = true - + // Add a small delay to ensure the server is ready setTimeout(() => { try { - console.log('๐Ÿ”Œ CloudflareAdapter: Creating WebSocket connection to:', wsUrl) this.websocket = new WebSocket(wsUrl) this.websocket.onopen = () => { - console.log('๐Ÿ”Œ CloudflareAdapter: WebSocket connection opened successfully') this.isConnecting = false this.reconnectAttempts = 0 this.setConnectionState('connected') this.readyResolve?.() this.startKeepAlive() - // CRITICAL: Emit 'ready' event for Automerge Repo - // This tells the Repo that the network adapter is ready to sync + // Emit 'ready' event for Automerge Repo // @ts-expect-error - 'ready' event is valid but not in NetworkAdapterEvents type this.emit('ready', { network: this }) // Create a server peer ID based on the room - // The server acts as a "hub" peer that all clients sync with this.serverPeerId = `server-${this.roomId}` as PeerId - // CRITICAL: Emit 'peer-candidate' to announce the server as a sync peer - // This tells the Automerge Repo there's a peer to sync documents with - console.log('๐Ÿ”Œ CloudflareAdapter: Announcing server peer for Automerge sync:', this.serverPeerId) + // Emit 'peer-candidate' to announce the server as a sync peer this.emit('peer-candidate', { peerId: this.serverPeerId, peerMetadata: { storageId: undefined, isEphemeral: false } @@ -367,16 +342,8 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { // Automerge's native protocol uses binary messages // We need to handle both binary and text messages if (event.data instanceof ArrayBuffer) { - console.log('๐Ÿ”Œ CloudflareAdapter: Received binary message (Automerge protocol)', event.data.byteLength, 'bytes') - // Handle binary Automerge sync messages - convert ArrayBuffer to Uint8Array - // Automerge Repo expects binary sync messages as Uint8Array - // CRITICAL: senderId should be the SERVER (where the message came from) - // targetId should be US (where the message is going to) - // CRITICAL: Include documentId for Automerge Repo to route the message correctly const binaryData = new Uint8Array(event.data) if (!this.currentDocumentId) { - console.log('๐Ÿ“ฆ CloudflareAdapter: Buffering binary sync message (no documentId yet), size:', binaryData.byteLength) - // Buffer for later processing when we have a documentId this.pendingBinaryMessages.push(binaryData) return } @@ -385,17 +352,13 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { data: binaryData, senderId: this.serverPeerId || ('server' as PeerId), targetId: this.peerId || ('unknown' as PeerId), - documentId: this.currentDocumentId as any // DocumentId type + documentId: this.currentDocumentId as any } - console.log('๐Ÿ“ฅ CloudflareAdapter: Emitting sync message with documentId:', this.currentDocumentId) this.emit('message', message) } else if (event.data instanceof Blob) { - // Handle Blob messages (convert to Uint8Array) event.data.arrayBuffer().then((buffer) => { - console.log('๐Ÿ”Œ CloudflareAdapter: Received Blob message, converted to Uint8Array', buffer.byteLength, 'bytes') const binaryData = new Uint8Array(buffer) if (!this.currentDocumentId) { - console.log('๐Ÿ“ฆ CloudflareAdapter: Buffering Blob sync message (no documentId yet), size:', binaryData.byteLength) this.pendingBinaryMessages.push(binaryData) return } @@ -406,18 +369,12 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { targetId: this.peerId || ('unknown' as PeerId), documentId: this.currentDocumentId as any } - console.log('๐Ÿ“ฅ CloudflareAdapter: Emitting Blob sync message with documentId:', this.currentDocumentId) this.emit('message', message) }) } else { // Handle text messages (our custom protocol for backward compatibility) const message = JSON.parse(event.data) - // Only log non-presence messages to reduce console spam - if (message.type !== 'presence' && message.type !== 'pong') { - console.log('๐Ÿ”Œ CloudflareAdapter: Received WebSocket message:', message.type) - } - // Handle ping/pong messages for keep-alive if (message.type === 'ping') { this.sendPong() @@ -426,13 +383,11 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { // Handle test messages if (message.type === 'test') { - console.log('๐Ÿ”Œ CloudflareAdapter: Received test message:', message.message) return } // Handle presence updates from other clients if (message.type === 'presence') { - // Pass senderId, userName, and userColor so we can create proper instance_presence records if (this.onPresenceUpdate && message.userId && message.data) { this.onPresenceUpdate(message.userId, message.data, message.senderId, message.userName, message.userColor) } @@ -441,49 +396,31 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { // Handle leave messages (user disconnected) if (message.type === 'leave') { - console.log('๐Ÿ‘‹ CloudflareAdapter: User left:', message.sessionId) if (this.onPresenceLeave && message.sessionId) { this.onPresenceLeave(message.sessionId) } return } - + // Convert the message to the format expected by Automerge if (message.type === 'sync' && message.data) { - console.log('๐Ÿ”Œ CloudflareAdapter: Received sync message with data:', { - hasStore: !!message.data.store, - storeKeys: message.data.store ? Object.keys(message.data.store).length : 0, - documentId: message.documentId, - documentIdType: typeof message.documentId - }) - // JSON sync for real-time collaboration - // When we receive TLDraw changes from other clients, apply them locally const isJsonDocumentData = message.data && typeof message.data === 'object' && message.data.store if (isJsonDocumentData) { - console.log('๐Ÿ“ฅ CloudflareAdapter: Received JSON sync message with store data') - - // Call the JSON sync callback to apply changes if (this.onJsonSyncData) { this.onJsonSyncData(message.data) - } else { - console.warn('โš ๏ธ No JSON sync callback registered') } - return // JSON sync handled + return } - - // Validate documentId - Automerge requires a valid Automerge URL format - // Valid formats: "automerge:xxxxx" or other valid URL formats - // Invalid: plain strings like "default", "default-room", etc. - const isValidDocumentId = message.documentId && - (typeof message.documentId === 'string' && - (message.documentId.startsWith('automerge:') || - message.documentId.includes(':') || - /^[a-f0-9-]{36,}$/i.test(message.documentId))) // UUID-like format - - // For binary sync messages, use Automerge's sync protocol - // Only include documentId if it's a valid Automerge document ID format + + // Validate documentId format + const isValidDocumentId = message.documentId && + (typeof message.documentId === 'string' && + (message.documentId.startsWith('automerge:') || + message.documentId.includes(':') || + /^[a-f0-9-]{36,}$/i.test(message.documentId))) + const syncMessage: Message = { type: 'sync', senderId: message.senderId || this.peerId || ('unknown' as PeerId), @@ -491,42 +428,22 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { data: message.data, ...(isValidDocumentId && { documentId: message.documentId }) } - - if (message.documentId && !isValidDocumentId) { - console.warn('โš ๏ธ CloudflareAdapter: Ignoring invalid documentId from server:', message.documentId) - } - + this.emit('message', syncMessage) } else if (message.senderId && message.targetId) { this.emit('message', message as Message) } } } catch (error) { - console.error('โŒ CloudflareAdapter: Error parsing WebSocket message:', error) + console.error('Error parsing WebSocket message:', error) } } this.websocket.onclose = (event) => { - console.log('Disconnected from Cloudflare WebSocket', { - code: event.code, - reason: event.reason, - wasClean: event.wasClean, - url: wsUrl, - reconnectAttempts: this.reconnectAttempts - }) - this.isConnecting = false this.stopKeepAlive() - // Log specific error codes for debugging - if (event.code === 1005) { - console.error('โŒ WebSocket closed with code 1005 (No Status Received) - this usually indicates a connection issue or idle timeout') - } else if (event.code === 1006) { - console.error('โŒ WebSocket closed with code 1006 (Abnormal Closure) - connection was lost unexpectedly') - } else if (event.code === 1011) { - console.error('โŒ WebSocket closed with code 1011 (Server Error) - server encountered an error') - } else if (event.code === 1000) { - console.log('โœ… WebSocket closed normally (code 1000)') + if (event.code === 1000) { this.setConnectionState('disconnected') return // Don't reconnect on normal closure } @@ -544,15 +461,7 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { this.scheduleReconnect(peerId, peerMetadata) } - this.websocket.onerror = (error) => { - console.error('WebSocket error:', error) - console.error('WebSocket readyState:', this.websocket?.readyState) - console.error('WebSocket URL:', wsUrl) - console.error('Error event details:', { - type: error.type, - target: error.target, - isTrusted: error.isTrusted - }) + this.websocket.onerror = () => { this.isConnecting = false } } catch (error) { @@ -564,25 +473,10 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { } send(message: Message): void { - // Only log non-presence messages to reduce console spam - if (message.type !== 'presence') { - console.log('๐Ÿ“ค CloudflareAdapter.send() called:', { - messageType: message.type, - dataType: (message as any).data?.constructor?.name || typeof (message as any).data, - dataLength: (message as any).data?.byteLength || (message as any).data?.length, - documentId: (message as any).documentId, - hasTargetId: !!message.targetId, - hasSenderId: !!message.senderId, - useBinarySync: this.useBinarySync - }) - } - - // CRITICAL: Capture documentId from outgoing sync messages - // This allows us to use it for incoming messages from the server + // Capture documentId from outgoing sync messages if (message.type === 'sync' && (message as any).documentId) { const docId = (message as any).documentId if (this.currentDocumentId !== docId) { - console.log('๐Ÿ“‹ CloudflareAdapter: Captured documentId from outgoing sync:', docId) this.currentDocumentId = docId } } @@ -590,49 +484,14 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { // Check if this is a binary sync message from Automerge Repo if (message.type === 'sync' && (message as any).data instanceof ArrayBuffer) { - console.log('๐Ÿ“ค CloudflareAdapter: Sending binary sync message (Automerge protocol)', { - dataLength: (message as any).data.byteLength, - documentId: (message as any).documentId, - targetId: message.targetId - }) - // Send binary data directly for Automerge's native sync protocol this.websocket.send((message as any).data) - return // CRITICAL: Don't fall through to JSON send + return } else if (message.type === 'sync' && (message as any).data instanceof Uint8Array) { - console.log('๐Ÿ“ค CloudflareAdapter: Sending Uint8Array sync message (Automerge protocol)', { - dataLength: (message as any).data.length, - documentId: (message as any).documentId, - targetId: message.targetId - }) - // Send Uint8Array directly - WebSocket accepts Uint8Array this.websocket.send((message as any).data) - return // CRITICAL: Don't fall through to JSON send + return } else { - // Handle text-based messages (backward compatibility and control messages) - // Only log non-presence messages - if (message.type !== 'presence') { - console.log('๐Ÿ“ค Sending WebSocket message:', message.type) - } - // Debug: Log patch content if it's a patch message - if (message.type === 'patch' && (message as any).patches) { - console.log('๐Ÿ” Sending patches:', (message as any).patches.length, 'patches') - ;(message as any).patches.forEach((patch: any, index: number) => { - console.log(` Patch ${index}:`, { - action: patch.action, - path: patch.path, - value: patch.value ? (typeof patch.value === 'object' ? 'object' : patch.value) : 'undefined' - }) - }) - } this.websocket.send(JSON.stringify(message)) } - } else { - if (message.type !== 'presence') { - console.warn('โš ๏ธ CloudflareAdapter: Cannot send message - WebSocket not open', { - messageType: message.type, - readyState: this.websocket?.readyState - }) - } } } @@ -669,7 +528,6 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { type: 'leave', sessionId: this.sessionId })) - console.log('๐Ÿ‘‹ CloudflareAdapter: Sent leave message for session:', this.sessionId) } catch (e) { // Ignore errors when sending leave message } @@ -683,13 +541,12 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { // Send ping every 30 seconds to prevent idle timeout this.keepAliveInterval = setInterval(() => { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { - console.log('๐Ÿ”Œ CloudflareAdapter: Sending keep-alive ping') this.websocket.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })) } - }, 30000) // 30 seconds + }, 30000) } private stopKeepAlive(): void { @@ -710,18 +567,14 @@ export class CloudflareNetworkAdapter extends NetworkAdapter { private scheduleReconnect(peerId: PeerId, peerMetadata?: PeerMetadata): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.error('โŒ CloudflareAdapter: Max reconnection attempts reached, giving up') return } this.reconnectAttempts++ - const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000) // Max 30 seconds - - console.log(`๐Ÿ”„ CloudflareAdapter: Scheduling reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`) - + const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000) + this.reconnectTimeout = setTimeout(() => { if (this.roomId) { - console.log(`๐Ÿ”„ CloudflareAdapter: Attempting reconnect ${this.reconnectAttempts}/${this.maxReconnectAttempts}`) this.connect(peerId, peerMetadata) } }, delay) diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index fabdc13..353979e 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -132,6 +132,10 @@ import { MultmuxShape } from "@/shapes/MultmuxShapeUtil" import { MycelialIntelligenceShape } from "@/shapes/MycelialIntelligenceShapeUtil" // Open Mapping - OSM map shape for geographic visualization import { MapShape } from "@/shapes/MapShapeUtil" +// Calendar shape for calendar functionality +import { CalendarShape } from "@/shapes/CalendarShapeUtil" +// Drawfast shape for quick drawing/sketching +import { DrawfastShape } from "@/shapes/DrawfastShapeUtil" export function useAutomergeStoreV2({ handle, @@ -169,6 +173,8 @@ export function useAutomergeStoreV2({ MultmuxShape, MycelialIntelligenceShape, // Deprecated - kept for backwards compatibility MapShape, // Open Mapping - OSM map shape + CalendarShape, // Calendar with view switching + DrawfastShape, // Drawfast quick sketching ] // CRITICAL: Explicitly list ALL custom shape types to ensure they're registered @@ -193,6 +199,8 @@ export function useAutomergeStoreV2({ 'Multmux', 'MycelialIntelligence', // Deprecated - kept for backwards compatibility 'Map', // Open Mapping - OSM map shape + 'Calendar', // Calendar with view switching + 'Drawfast', // Drawfast quick sketching ] // Build schema with explicit entries for all custom shapes diff --git a/src/components/ObsidianVaultBrowser.tsx b/src/components/ObsidianVaultBrowser.tsx index e49f13e..5710ed6 100644 --- a/src/components/ObsidianVaultBrowser.tsx +++ b/src/components/ObsidianVaultBrowser.tsx @@ -82,58 +82,48 @@ export const ObsidianVaultBrowser: React.FC = ({ // Save vault to Automerge store const saveVaultToAutomerge = (vault: ObsidianVault) => { if (!automergeHandle) { - console.warn('โš ๏ธ Automerge handle not available, saving to localStorage only') try { const vaultRecord = importer.vaultToRecord(vault) localStorage.setItem(`obsidian_vault_cache:${vault.name}`, JSON.stringify({ ...vaultRecord, lastImported: vaultRecord.lastImported instanceof Date ? vaultRecord.lastImported.toISOString() : vaultRecord.lastImported })) - console.log('๐Ÿ”ง Saved vault to localStorage (Automerge handle not available):', vaultRecord.id) } catch (localStorageError) { - console.warn('โš ๏ธ Could not save vault to localStorage:', localStorageError) + console.warn('Could not save vault to localStorage:', localStorageError) } return } - + try { const vaultRecord = importer.vaultToRecord(vault) - + // Save directly to Automerge, bypassing TLDraw store validation - // This allows us to save custom record types like obsidian_vault automergeHandle.change((doc: any) => { - // Ensure doc.store exists if (!doc.store) { doc.store = {} } - - // Save the vault record directly to Automerge store - // Convert Date to ISO string for serialization + const recordToSave = { ...vaultRecord, - lastImported: vaultRecord.lastImported instanceof Date - ? vaultRecord.lastImported.toISOString() + lastImported: vaultRecord.lastImported instanceof Date + ? vaultRecord.lastImported.toISOString() : vaultRecord.lastImported } - + doc.store[vaultRecord.id] = recordToSave }) - - console.log('๐Ÿ”ง Saved vault to Automerge:', vaultRecord.id) - + // Also save to localStorage as a backup try { localStorage.setItem(`obsidian_vault_cache:${vault.name}`, JSON.stringify({ ...vaultRecord, lastImported: vaultRecord.lastImported instanceof Date ? vaultRecord.lastImported.toISOString() : vaultRecord.lastImported })) - console.log('๐Ÿ”ง Saved vault to localStorage as backup:', vaultRecord.id) } catch (localStorageError) { - console.warn('โš ๏ธ Could not save vault to localStorage:', localStorageError) + // Silent fail for backup } } catch (error) { - console.error('โŒ Error saving vault to Automerge:', error) - // Don't throw - allow vault loading to continue even if saving fails + console.error('Error saving vault to Automerge:', error) // Try localStorage as fallback try { const vaultRecord = importer.vaultToRecord(vault) @@ -141,9 +131,8 @@ export const ObsidianVaultBrowser: React.FC = ({ ...vaultRecord, lastImported: vaultRecord.lastImported instanceof Date ? vaultRecord.lastImported.toISOString() : vaultRecord.lastImported })) - console.log('๐Ÿ”ง Saved vault to localStorage as fallback:', vaultRecord.id) } catch (localStorageError) { - console.warn('โš ๏ธ Could not save vault to localStorage:', localStorageError) + console.warn('Could not save vault to localStorage:', localStorageError) } } } @@ -157,10 +146,8 @@ export const ObsidianVaultBrowser: React.FC = ({ if (doc && doc.store) { const vaultId = `obsidian_vault:${vaultName}` const vaultRecord = doc.store[vaultId] as ObsidianVaultRecord | undefined - + if (vaultRecord && vaultRecord.typeName === 'obsidian_vault') { - console.log('๐Ÿ”ง Loaded vault from Automerge:', vaultId) - // Convert date string back to Date object if needed const recordCopy = JSON.parse(JSON.stringify(vaultRecord)) if (typeof recordCopy.lastImported === 'string') { recordCopy.lastImported = new Date(recordCopy.lastImported) @@ -169,18 +156,16 @@ export const ObsidianVaultBrowser: React.FC = ({ } } } catch (error) { - console.warn('โš ๏ธ Could not load vault from Automerge:', error) + // Fall through to localStorage } } - + // Try localStorage as fallback try { const cached = localStorage.getItem(`obsidian_vault_cache:${vaultName}`) if (cached) { const vaultRecord = JSON.parse(cached) as ObsidianVaultRecord if (vaultRecord && vaultRecord.typeName === 'obsidian_vault') { - console.log('๐Ÿ”ง Loaded vault from localStorage cache:', vaultName) - // Convert date string back to Date object if (typeof vaultRecord.lastImported === 'string') { vaultRecord.lastImported = new Date(vaultRecord.lastImported) } @@ -188,9 +173,9 @@ export const ObsidianVaultBrowser: React.FC = ({ } } } catch (e) { - console.warn('โš ๏ธ Could not load vault from localStorage:', e) + // Silent fail } - + return null } @@ -198,47 +183,31 @@ export const ObsidianVaultBrowser: React.FC = ({ useEffect(() => { // Prevent multiple loads if already loading or already loaded once if (isLoadingVault || hasLoadedOnce) { - console.log('๐Ÿ”ง ObsidianVaultBrowser: Skipping load - already loading or loaded once') return } - console.log('๐Ÿ”ง ObsidianVaultBrowser: Component mounted, checking user identity for vault...') - console.log('๐Ÿ”ง Current session vault data:', { - path: session.obsidianVaultPath, - name: session.obsidianVaultName, - authed: session.authed, - username: session.username - }) - // FIRST PRIORITY: Try to load from user's configured vault in session (user identity) if (session.obsidianVaultPath && session.obsidianVaultPath !== 'folder-selected') { - console.log('โœ… Found configured vault in user identity:', session.obsidianVaultPath) - console.log('๐Ÿ”ง Loading vault from user identity...') - // First try to load from Automerge cache for faster loading if (session.obsidianVaultName) { const cachedVault = loadVaultFromAutomerge(session.obsidianVaultName) if (cachedVault) { - console.log('โœ… Loaded vault from Automerge cache') setVault(cachedVault) setIsLoading(false) setHasLoadedOnce(true) return } } - + // If not in cache, load from source (Quartz URL or local path) - console.log('๐Ÿ”ง Loading vault from source:', session.obsidianVaultPath) loadVault(session.obsidianVaultPath) } else if (session.obsidianVaultPath === 'folder-selected' && session.obsidianVaultName) { - console.log('๐Ÿ”ง Vault was previously selected via folder picker, showing reselect interface') // For folder-selected vaults, we can't reload them, so show a special reselect interface setVault(null) setShowFolderReselect(true) setIsLoading(false) setHasLoadedOnce(true) } else { - console.log('โš ๏ธ No vault configured in user identity, showing empty state...') setVault(null) setIsLoading(false) setHasLoadedOnce(true) @@ -250,30 +219,28 @@ export const ObsidianVaultBrowser: React.FC = ({ // Check if values actually changed (not just object reference) const vaultPathChanged = previousVaultPathRef.current !== session.obsidianVaultPath const vaultNameChanged = previousVaultNameRef.current !== session.obsidianVaultName - + // If vault is already loaded and values haven't changed, don't do anything if (hasLoadedOnce && !vaultPathChanged && !vaultNameChanged) { - return // Already loaded and nothing changed, no need to reload + return } - + // Update refs to current values previousVaultPathRef.current = session.obsidianVaultPath previousVaultNameRef.current = session.obsidianVaultName - + // Only proceed if values actually changed and we haven't loaded yet if (!vaultPathChanged && !vaultNameChanged) { - return // Values haven't changed, no need to reload + return } - + if (hasLoadedOnce || isLoadingVault) { - return // Don't reload if we've already loaded or are currently loading + return } if (session.obsidianVaultPath && session.obsidianVaultPath !== 'folder-selected') { - console.log('๐Ÿ”ง Session vault path changed, loading vault:', session.obsidianVaultPath) loadVault(session.obsidianVaultPath) } else if (session.obsidianVaultPath === 'folder-selected' && session.obsidianVaultName) { - console.log('๐Ÿ”ง Session shows folder-selected vault, showing reselect interface') setVault(null) setShowFolderReselect(true) setIsLoading(false) @@ -284,7 +251,6 @@ export const ObsidianVaultBrowser: React.FC = ({ // Auto-open folder picker if requested useEffect(() => { if (autoOpenFolderPicker) { - console.log('Auto-opening folder picker...') handleFolderPicker() } }, [autoOpenFolderPicker]) @@ -312,7 +278,6 @@ export const ObsidianVaultBrowser: React.FC = ({ useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - console.log('๐Ÿ”ง ESC key pressed, closing vault browser') onClose() } } @@ -326,57 +291,38 @@ export const ObsidianVaultBrowser: React.FC = ({ const loadVault = async (path?: string) => { // Prevent concurrent loading operations if (isLoadingVault) { - console.log('๐Ÿ”ง loadVault: Already loading, skipping concurrent request') return } setIsLoadingVault(true) setIsLoading(true) setError(null) - + try { if (path) { // Check if it's a Quartz URL if (path.startsWith('http') || path.includes('quartz') || path.includes('.xyz') || path.includes('.com')) { - // Load from Quartz URL - always get latest data - console.log('๐Ÿ”ง Loading Quartz vault from URL (getting latest data):', path) const loadedVault = await importer.importFromQuartzUrl(path) - console.log('Loaded Quartz vault from URL:', loadedVault) setVault(loadedVault) setShowVaultInput(false) setShowFolderReselect(false) - // Save the vault path and name to user session - console.log('๐Ÿ”ง Saving Quartz vault to session:', { path, name: loadedVault.name }) - updateSession({ + updateSession({ obsidianVaultPath: path, obsidianVaultName: loadedVault.name }) - console.log('๐Ÿ”ง Quartz vault saved to session successfully') - - // Save vault to Automerge for persistence saveVaultToAutomerge(loadedVault) } else { - // Load from local directory - console.log('๐Ÿ”ง Loading vault from local directory:', path) const loadedVault = await importer.importFromDirectory(path) - console.log('Loaded vault from path:', loadedVault) setVault(loadedVault) setShowVaultInput(false) setShowFolderReselect(false) - // Save the vault path and name to user session - console.log('๐Ÿ”ง Saving vault to session:', { path, name: loadedVault.name }) - updateSession({ + updateSession({ obsidianVaultPath: path, obsidianVaultName: loadedVault.name }) - console.log('๐Ÿ”ง Vault saved to session successfully') - - // Save vault to Automerge for persistence saveVaultToAutomerge(loadedVault) } } else { - // No vault configured - show empty state - console.log('No vault configured, showing empty state...') setVault(null) setShowVaultInput(false) } @@ -384,8 +330,6 @@ export const ObsidianVaultBrowser: React.FC = ({ console.error('Failed to load vault:', err) setError('Failed to load Obsidian vault. Please try again.') setVault(null) - // Don't show vault input if user already has a vault configured - // Only show vault input if this is a fresh attempt if (!session.obsidianVaultPath) { setShowVaultInput(true) } @@ -401,11 +345,8 @@ export const ObsidianVaultBrowser: React.FC = ({ setError('Please enter a vault path or URL') return } - - console.log('๐Ÿ“ Submitting vault path:', vaultPath.trim(), 'Method:', inputMethod) - + if (inputMethod === 'quartz') { - // Handle Quartz URL try { setIsLoading(true) setError(null) @@ -413,70 +354,49 @@ export const ObsidianVaultBrowser: React.FC = ({ setVault(loadedVault) setShowVaultInput(false) setShowFolderReselect(false) - - // Save Quartz vault to user identity (session) - console.log('๐Ÿ”ง Saving Quartz vault to user identity:', { - path: vaultPath.trim(), - name: loadedVault.name - }) - updateSession({ + updateSession({ obsidianVaultPath: vaultPath.trim(), obsidianVaultName: loadedVault.name }) } catch (error) { - console.error('โŒ Error loading Quartz vault:', error) + console.error('Error loading Quartz vault:', error) setError(error instanceof Error ? error.message : 'Failed to load Quartz vault') } finally { setIsLoading(false) } } else { - // Handle regular vault path (local folder or URL) loadVault(vaultPath.trim()) } } const handleFolderPicker = async () => { - console.log('๐Ÿ“ Folder picker button clicked') - if (!('showDirectoryPicker' in window)) { setError('File System Access API is not supported in this browser. Please use "Enter Path" instead.') setShowVaultInput(true) return } - + try { setIsLoading(true) setError(null) - console.log('๐Ÿ“ Opening directory picker...') - + const loadedVault = await importer.importFromFileSystem() - console.log('โœ… Vault loaded from folder picker:', loadedVault.name) - + setVault(loadedVault) setShowVaultInput(false) setShowFolderReselect(false) - - // Note: We can't get the actual path from importFromFileSystem, - // but we can save a flag that a folder was selected - console.log('๐Ÿ”ง Saving folder-selected vault to user identity:', { - path: 'folder-selected', - name: loadedVault.name - }) - updateSession({ + + updateSession({ obsidianVaultPath: 'folder-selected', obsidianVaultName: loadedVault.name }) - console.log('โœ… Folder-selected vault saved to user identity successfully') - - // Save vault to Automerge for persistence + saveVaultToAutomerge(loadedVault) } catch (err) { - console.error('โŒ Failed to load vault from folder picker:', err) if ((err as any).name === 'AbortError') { - // User cancelled the folder picker - console.log('๐Ÿ“ User cancelled folder picker') - setError(null) // Don't show error for cancellation + setError(null) } else { + console.error('Failed to load vault from folder picker:', err) setError('Failed to load Obsidian vault. Please try again.') } } finally { @@ -514,45 +434,27 @@ export const ObsidianVaultBrowser: React.FC = ({ const folderNotes = importer.getAllNotesFromTree(folder) obs_notes = obs_notes.filter(note => folderNotes.some(folderNote => folderNote.id === note.id)) } - } else if (viewMode === 'tree' && selectedFolder === null) { - // In tree view but no folder selected, show all notes - // This allows users to see all notes when no specific folder is selected } - // Debug logging - console.log('Search query:', debouncedSearchQuery) - console.log('View mode:', viewMode) - console.log('Selected folder:', selectedFolder) - console.log('Total notes:', vault.obs_notes.length) - console.log('Filtered notes:', obs_notes.length) - return obs_notes }, [vault, debouncedSearchQuery, viewMode, selectedFolder, folderTree, importer]) // Listen for trigger-obsnote-creation event from CustomToolbar useEffect(() => { const handleTriggerCreation = () => { - console.log('๐ŸŽฏ ObsidianVaultBrowser: Received trigger-obsnote-creation event') - if (selectedNotes.size > 0) { - // Create shapes from currently selected notes const selectedObsNotes = filteredObsNotes.filter(obs_note => selectedNotes.has(obs_note.id)) - console.log('๐ŸŽฏ Creating shapes from selected notes:', selectedObsNotes.length) onObsNotesSelect(selectedObsNotes) } else { - // If no notes are selected, select all visible notes const allVisibleNotes = filteredObsNotes if (allVisibleNotes.length > 0) { - console.log('๐ŸŽฏ No notes selected, creating shapes from all visible notes:', allVisibleNotes.length) onObsNotesSelect(allVisibleNotes) - } else { - console.log('๐ŸŽฏ No notes available to create shapes from') } } } window.addEventListener('trigger-obsnote-creation', handleTriggerCreation as EventListener) - + return () => { window.removeEventListener('trigger-obsnote-creation', handleTriggerCreation as EventListener) } @@ -663,7 +565,6 @@ export const ObsidianVaultBrowser: React.FC = ({ } const handleObsNoteClick = (obs_note: ObsidianObsNote) => { - console.log('๐ŸŽฏ ObsidianVaultBrowser: handleObsNoteClick called with:', obs_note) onObsNoteSelect(obs_note) } @@ -679,7 +580,6 @@ export const ObsidianVaultBrowser: React.FC = ({ const handleBulkImport = () => { const selectedObsNotes = filteredObsNotes.filter(obs_note => selectedNotes.has(obs_note.id)) - console.log('๐ŸŽฏ ObsidianVaultBrowser: handleBulkImport called with:', selectedObsNotes.length, 'notes') onObsNotesSelect(selectedObsNotes) setSelectedNotes(new Set()) } @@ -730,13 +630,11 @@ export const ObsidianVaultBrowser: React.FC = ({ const handleDisconnectVault = () => { - // Clear the vault from session - updateSession({ + updateSession({ obsidianVaultPath: undefined, obsidianVaultName: undefined }) - - // Reset component state + setVault(null) setSearchQuery('') setDebouncedSearchQuery('') @@ -746,8 +644,6 @@ export const ObsidianVaultBrowser: React.FC = ({ setError(null) setHasLoadedOnce(false) setIsLoadingVault(false) - - console.log('๐Ÿ”ง Vault disconnected successfully') } const handleBackdropClick = (e: React.MouseEvent) => { @@ -841,24 +737,19 @@ export const ObsidianVaultBrowser: React.FC = ({

Load Obsidian Vault

Choose how you'd like to load your Obsidian vault:

- -