From 75d67623ce989c5d2f862bd8eb8b0b36af800010 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 6 Mar 2026 21:28:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Pulse:=20fix=20slow=20note=20open?= =?UTF-8?q?=20=E2=80=94=20O(1)=20map=20lookup,=20no=20reloadVault=20on=20c?= =?UTF-8?q?lick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: clicking a note in Pulse used an inline arrow function that: 1. Was recreated on every render (new prop ref → PulseView memo bypassed) 2. Called vault.reloadVault() (full 9000-note rescan) when path didn't match Fix: - Add entriesByPath Map (useMemo) — O(1) lookups instead of O(n) .find() - Add handlePulseOpenNote (useCallback) — stable ref, never triggers reloadVault (Pulse notes always exist in vault; no reload needed) - Wire PulseView to handlePulseOpenNote instead of inline arrow - Also use entriesByPath in openNoteByPath (MCP bridge) --- src/App.tsx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 759af138..135ffdc0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -230,19 +230,26 @@ function App() { useNavigationGestures({ onGoBack: handleGoBack, onGoForward: handleGoForward }) + // O(1) path lookup map — rebuilt only when vault.entries changes + const entriesByPath = useMemo(() => { + const map = new Map() + for (const e of vault.entries) map.set(e.path, e) + return map + }, [vault.entries]) + // MCP UI bridge: react to AI-driven open/highlight/vault-change events const openNoteByPath = useCallback((path: string) => { - const entry = vault.entries.find(e => e.path === path || e.path === `${resolvedPath}/${path}`) + const entry = entriesByPath.get(path) ?? entriesByPath.get(`${resolvedPath}/${path}`) if (entry) { notes.handleSelectNote(entry) } else { // Entry not yet in vault (just created) — reload then open vault.reloadVault().then(freshEntries => { - const fresh = freshEntries.find((e: VaultEntry) => e.path === path || e.path === `${resolvedPath}/${path}`) + const fresh = (freshEntries as VaultEntry[]).find(e => e.path === path || e.path === `${resolvedPath}/${path}`) if (fresh) notes.handleSelectNote(fresh) }) } - }, [vault, notes, resolvedPath]) + }, [entriesByPath, vault, notes, resolvedPath]) const aiActivity = useAiActivity({ onOpenNote: openNoteByPath, @@ -253,10 +260,18 @@ function App() { onVaultChanged: () => { vault.reloadVault() }, }) + // Stable callback for Pulse "open note" — never triggers reloadVault. + // Pulse files always exist in the vault; if somehow not found, silently skip. + const handlePulseOpenNote = useCallback((relativePath: string) => { + const fullPath = `${resolvedPath}/${relativePath}` + const entry = entriesByPath.get(fullPath) ?? entriesByPath.get(relativePath) + if (entry) notes.handleSelectNote(entry) + }, [entriesByPath, resolvedPath, notes]) + // Agent file operation handlers: auto-open created notes, live-refresh modified notes const handleAgentFileCreated = useCallback((relativePath: string) => { vault.reloadVault().then(freshEntries => { - const entry = freshEntries.find((e: VaultEntry) => e.path === relativePath || e.path === `${resolvedPath}/${relativePath}`) + const entry = (freshEntries as VaultEntry[]).find(e => e.path === relativePath || e.path === `${resolvedPath}/${relativePath}`) if (entry) notes.handleSelectNote(entry) }) }, [vault, notes, resolvedPath]) @@ -509,11 +524,7 @@ function App() { <>
{selection.kind === 'filter' && selection.filter === 'pulse' ? ( - { - const fullPath = `${resolvedPath}/${relativePath}` - const entry = vault.entries.find(e => e.path === fullPath || e.path === relativePath) - if (entry) notes.handleSelectNote(entry) - }} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => setViewMode('all')} /> + setViewMode('all')} /> ) : ( )}