diff --git a/src/App.tsx b/src/App.tsx index f5460ce2..03db4660 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -615,8 +615,14 @@ function App() { }, [handleEnterNeighborhood, handleReplaceActiveTab]) const vaultBridge = useVaultBridge({ - entriesByPath, resolvedPath, + entriesByPath, + resolvedPath, reloadVault: vault.reloadVault, + reloadFolders: vault.reloadFolders, + reloadViews: vault.reloadViews, + closeAllTabs, + replaceActiveTab: handleReplaceActiveTab, + hasUnsavedChanges: (path) => vault.unsavedPaths.has(path), onSelectNote: notes.handleSelectNote, activeTabPath: notes.activeTabPath, }) diff --git a/src/hooks/useVaultBridge.test.ts b/src/hooks/useVaultBridge.test.ts index ae871284..7a8968d3 100644 --- a/src/hooks/useVaultBridge.test.ts +++ b/src/hooks/useVaultBridge.test.ts @@ -7,22 +7,54 @@ function makeEntry(path: string, title = 'Test'): VaultEntry { return { path, title, filename: path.split('/').pop()!, content: '', outgoingLinks: [], snippet: '', wordCount: 0, isA: 'Note', status: null, createdAt: null, modifiedAt: null, icon: null, tags: [] } as unknown as VaultEntry } +function expectVaultDerivedStateReloaded(options: { + reloadVault: ReturnType + reloadFolders: ReturnType + reloadViews: ReturnType +}) { + const { reloadVault, reloadFolders, reloadViews } = options + expect(reloadVault).toHaveBeenCalledOnce() + expect(reloadFolders).toHaveBeenCalledOnce() + expect(reloadViews).toHaveBeenCalledOnce() +} + describe('useVaultBridge', () => { const onSelectNote = vi.fn() let reloadVault: ReturnType + let reloadFolders: ReturnType + let reloadViews: ReturnType + let closeAllTabs: ReturnType + let replaceActiveTab: ReturnType + let hasUnsavedChanges: ReturnType beforeEach(() => { vi.clearAllMocks() reloadVault = vi.fn().mockResolvedValue([]) + reloadFolders = vi.fn() + reloadViews = vi.fn() + closeAllTabs = vi.fn() + replaceActiveTab = vi.fn().mockResolvedValue(undefined) + hasUnsavedChanges = vi.fn(() => false) }) - function renderBridge(entries: VaultEntry[] = [], activeTabPath: string | null = null) { + function renderBridge( + entries: VaultEntry[] = [], + activeTabPath: string | null = null, + overrides: Partial<{ + hasUnsavedChanges: typeof hasUnsavedChanges + }> = {}, + ) { const entriesByPath = new Map(entries.map(e => [e.path, e])) return renderHook(() => useVaultBridge({ entriesByPath, resolvedPath: '/vault', reloadVault, + reloadFolders, + reloadViews, + closeAllTabs, + replaceActiveTab, + hasUnsavedChanges: overrides.hasUnsavedChanges ?? hasUnsavedChanges, onSelectNote, activeTabPath, }), @@ -87,27 +119,52 @@ describe('useVaultBridge', () => { expect(onSelectNote).toHaveBeenCalledWith(fresh) }) - it('handleAgentFileModified reloads when active tab matches', () => { + it('handleAgentFileModified refreshes the active tab with fresh disk content', async () => { + const fresh = makeEntry('/vault/active.md', 'Fresh active') + reloadVault.mockResolvedValue([fresh]) const { result } = renderBridge([], '/vault/active.md') - act(() => { result.current.handleAgentFileModified('active.md') }) + await act(async () => { result.current.handleAgentFileModified('active.md') }) - expect(reloadVault).toHaveBeenCalled() + expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews }) + expect(closeAllTabs).toHaveBeenCalledOnce() + expect(replaceActiveTab).toHaveBeenCalledWith(fresh) }) - it('handleAgentFileModified does not reload for different tab', () => { + it('handleAgentFileModified still refreshes vault-derived UI for other notes', async () => { + const active = makeEntry('/vault/other.md', 'Other') + reloadVault.mockResolvedValue([active]) const { result } = renderBridge([], '/vault/other.md') - act(() => { result.current.handleAgentFileModified('active.md') }) + await act(async () => { result.current.handleAgentFileModified('active.md') }) - expect(reloadVault).not.toHaveBeenCalled() + expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews }) + expect(closeAllTabs).not.toHaveBeenCalled() + expect(replaceActiveTab).toHaveBeenCalledWith(active) }) - it('handleAgentVaultChanged always reloads', () => { - const { result } = renderBridge([]) + it('keeps unsaved active note content intact while reloading agent changes', async () => { + const fresh = makeEntry('/vault/active.md', 'Fresh active') + reloadVault.mockResolvedValue([fresh]) + const hasUnsaved = vi.fn((path: string) => path === '/vault/active.md') + const { result } = renderBridge([], '/vault/active.md', { hasUnsavedChanges: hasUnsaved }) - act(() => { result.current.handleAgentVaultChanged() }) + await act(async () => { result.current.handleAgentFileModified('active.md') }) - expect(reloadVault).toHaveBeenCalled() + expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews }) + expect(closeAllTabs).not.toHaveBeenCalled() + expect(replaceActiveTab).not.toHaveBeenCalled() + }) + + it('handleAgentVaultChanged reloads vault-derived state and refreshes the active note when safe', async () => { + const fresh = makeEntry('/vault/active.md', 'Fresh active') + reloadVault.mockResolvedValue([fresh]) + const { result } = renderBridge([], '/vault/active.md') + + await act(async () => { result.current.handleAgentVaultChanged() }) + + expectVaultDerivedStateReloaded({ reloadVault, reloadFolders, reloadViews }) + expect(closeAllTabs).not.toHaveBeenCalled() + expect(replaceActiveTab).toHaveBeenCalledWith(fresh) }) }) diff --git a/src/hooks/useVaultBridge.ts b/src/hooks/useVaultBridge.ts index 9e8e1d17..9f3e5e71 100644 --- a/src/hooks/useVaultBridge.ts +++ b/src/hooks/useVaultBridge.ts @@ -1,10 +1,16 @@ import { useCallback } from 'react' import type { VaultEntry } from '../types' +import { refreshPulledVaultState } from '../utils/pulledVaultRefresh' interface VaultBridgeDeps { entriesByPath: Map resolvedPath: string - reloadVault: () => Promise + reloadVault: () => Promise + reloadFolders: () => Promise | unknown + reloadViews: () => Promise | unknown + closeAllTabs: () => void + replaceActiveTab: (entry: VaultEntry) => Promise + hasUnsavedChanges: (path: string) => boolean onSelectNote: (entry: VaultEntry) => void activeTabPath: string | null } @@ -13,12 +19,21 @@ function findEntry(entriesByPath: Map, resolvedPath: string, return entriesByPath.get(path) ?? entriesByPath.get(`${resolvedPath}/${path}`) } -function findInFresh(entries: unknown, resolvedPath: string, path: string): VaultEntry | undefined { - return (entries as VaultEntry[]).find(e => e.path === path || e.path === `${resolvedPath}/${path}`) +function findInFresh(entries: VaultEntry[], resolvedPath: string, path: string): VaultEntry | undefined { + return entries.find(e => e.path === path || e.path === `${resolvedPath}/${path}`) } export function useVaultBridge({ - entriesByPath, resolvedPath, reloadVault, onSelectNote, activeTabPath, + entriesByPath, + resolvedPath, + reloadVault, + reloadFolders, + reloadViews, + closeAllTabs, + replaceActiveTab, + hasUnsavedChanges, + onSelectNote, + activeTabPath, }: VaultBridgeDeps) { const reloadAndOpen = useCallback((path: string) => { reloadVault().then(fresh => { @@ -27,6 +42,29 @@ export function useVaultBridge({ }) }, [reloadVault, onSelectNote, resolvedPath]) + const refreshAgentChanges = useCallback((updatedFiles: string[]) => ( + refreshPulledVaultState({ + activeTabPath, + closeAllTabs, + hasUnsavedChanges, + reloadFolders, + reloadVault, + reloadViews, + replaceActiveTab, + updatedFiles, + vaultPath: resolvedPath, + }) + ), [ + activeTabPath, + closeAllTabs, + hasUnsavedChanges, + reloadFolders, + reloadVault, + reloadViews, + replaceActiveTab, + resolvedPath, + ]) + const openNoteByPath = useCallback((path: string) => { const entry = findEntry(entriesByPath, resolvedPath, path) if (entry) onSelectNote(entry) @@ -40,11 +78,12 @@ export function useVaultBridge({ }, [entriesByPath, resolvedPath, onSelectNote]) const handleAgentFileModified = useCallback((relativePath: string) => { - const fullPath = `${resolvedPath}/${relativePath}` - if (activeTabPath === relativePath || activeTabPath === fullPath) reloadVault() - }, [reloadVault, activeTabPath, resolvedPath]) + void refreshAgentChanges([relativePath]) + }, [refreshAgentChanges]) - const handleAgentVaultChanged = useCallback(() => { reloadVault() }, [reloadVault]) + const handleAgentVaultChanged = useCallback(() => { + void refreshAgentChanges([]) + }, [refreshAgentChanges]) return { openNoteByPath,