fix: reload notes after agent edits

This commit is contained in:
lucaronin
2026-04-20 22:04:38 +02:00
parent a199fad803
commit eacf05a9f8
3 changed files with 122 additions and 20 deletions

View File

@@ -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,
})

View File

@@ -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)
})
})

View File

@@ -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,