fix: reload notes after agent edits
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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<typeof vi.fn>
|
||||
reloadFolders: ReturnType<typeof vi.fn>
|
||||
reloadViews: ReturnType<typeof vi.fn>
|
||||
}) {
|
||||
const { reloadVault, reloadFolders, reloadViews } = options
|
||||
expect(reloadVault).toHaveBeenCalledOnce()
|
||||
expect(reloadFolders).toHaveBeenCalledOnce()
|
||||
expect(reloadViews).toHaveBeenCalledOnce()
|
||||
}
|
||||
|
||||
describe('useVaultBridge', () => {
|
||||
const onSelectNote = vi.fn()
|
||||
let reloadVault: ReturnType<typeof vi.fn>
|
||||
let reloadFolders: ReturnType<typeof vi.fn>
|
||||
let reloadViews: ReturnType<typeof vi.fn>
|
||||
let closeAllTabs: ReturnType<typeof vi.fn>
|
||||
let replaceActiveTab: ReturnType<typeof vi.fn>
|
||||
let hasUnsavedChanges: ReturnType<typeof vi.fn>
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { refreshPulledVaultState } from '../utils/pulledVaultRefresh'
|
||||
|
||||
interface VaultBridgeDeps {
|
||||
entriesByPath: Map<string, VaultEntry>
|
||||
resolvedPath: string
|
||||
reloadVault: () => Promise<unknown>
|
||||
reloadVault: () => Promise<VaultEntry[]>
|
||||
reloadFolders: () => Promise<unknown> | unknown
|
||||
reloadViews: () => Promise<unknown> | unknown
|
||||
closeAllTabs: () => void
|
||||
replaceActiveTab: (entry: VaultEntry) => Promise<void>
|
||||
hasUnsavedChanges: (path: string) => boolean
|
||||
onSelectNote: (entry: VaultEntry) => void
|
||||
activeTabPath: string | null
|
||||
}
|
||||
@@ -13,12 +19,21 @@ function findEntry(entriesByPath: Map<string, VaultEntry>, 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,
|
||||
|
||||
Reference in New Issue
Block a user