diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index e1791da9..522dbe4d 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -1,4 +1,6 @@ import { useCallback } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { isTauri } from '../mock-tauri' import type { VaultEntry } from '../types' import type { FrontmatterValue } from '../components/Inspector' import { useTabManagement } from './useTabManagement' @@ -78,15 +80,27 @@ export function useNoteActions(config: NoteActionsConfig) { setTabs((prev) => prev.map((t) => t.entry.path === path ? { ...t, content: newContent } : t)) }, [setTabs]) - const creation = useNoteCreation(config, { openTabWithContent, handleSelectNote, handleCloseTab, handleCloseTabRef }) + // After opening a note, reload its VaultEntry so title reflects any sync. + const handleSelectNoteWithSync = useCallback(async (entry: VaultEntry) => { + await handleSelectNote(entry) + // Reload entry from disk to pick up title changes from sync_note_title + if (isTauri()) { + try { + const fresh = await invoke('reload_vault_entry', { path: entry.path }) + if (fresh.title !== entry.title) updateEntry(entry.path, { title: fresh.title }) + } catch { /* non-fatal: entry display may be stale */ } + } + }, [handleSelectNote, updateEntry]) + + const creation = useNoteCreation(config, { openTabWithContent, handleSelectNote: handleSelectNoteWithSync, handleCloseTab, handleCloseTabRef }) const rename = useNoteRename( { entries, setToastMessage }, { tabs: tabMgmt.tabs, setTabs, activeTabPathRef, handleSwitchTab, updateTabContent }, ) const handleNavigateWikilink = useCallback( - (target: string) => navigateWikilink(entries, target, handleSelectNote), - [entries, handleSelectNote], + (target: string) => navigateWikilink(entries, target, handleSelectNoteWithSync), + [entries, handleSelectNoteWithSync], ) const runFrontmatterOp = useCallback( @@ -97,6 +111,7 @@ export function useNoteActions(config: NoteActionsConfig) { return { ...tabMgmt, + handleSelectNote: handleSelectNoteWithSync, handleCloseTab: creation.handleCloseTabWithCleanup, handleNavigateWikilink, handleCreateNote: creation.handleCreateNote, diff --git a/src/hooks/useTabManagement.test.ts b/src/hooks/useTabManagement.test.ts index 5b9c685c..4995ce47 100644 --- a/src/hooks/useTabManagement.test.ts +++ b/src/hooks/useTabManagement.test.ts @@ -559,14 +559,17 @@ describe('useTabManagement', () => { // Start loading A (don't await — simulates rapid click) let selectADone = false - act(() => { + await act(async () => { result.current.handleSelectNote(makeEntry({ path: '/vault/a.md', title: 'A' })).then(() => { selectADone = true }) + // Flush microtask from sync_note_title (no-op in mock mode) so loadAndSetTab starts + await Promise.resolve() }) // Start loading B while A is still loading let selectBDone = false - act(() => { + await act(async () => { result.current.handleSelectNote(makeEntry({ path: '/vault/b.md', title: 'B' })).then(() => { selectBDone = true }) + await Promise.resolve() }) // B resolves first diff --git a/src/hooks/useTabManagement.ts b/src/hooks/useTabManagement.ts index baee22c6..66713112 100644 --- a/src/hooks/useTabManagement.ts +++ b/src/hooks/useTabManagement.ts @@ -69,6 +69,16 @@ async function loadNoteContent(path: string): Promise { : mockInvoke('get_note_content', { path }) } +/** Sync title frontmatter with filename on note open. + * Returns true if the file was modified. */ +async function syncNoteTitle(path: string): Promise { + try { + return isTauri() + ? await invoke('sync_note_title', { path }) + : false // mock: no-op + } catch { return false } +} + function addTabIfAbsent(prev: Tab[], entry: VaultEntry, content: string): Tab[] { if (prev.some((t) => t.entry.path === entry.path)) return prev return [...prev, { entry, content }] @@ -147,6 +157,8 @@ export function useTabManagement() { const handleSelectNote = useCallback(async (entry: VaultEntry) => { if (isTabOpen(tabsRef.current, entry.path)) { setActiveTabPath(entry.path); return } const seq = ++navSeqRef.current + // Sync title frontmatter with filename before loading content + await syncNoteTitle(entry.path) await loadAndSetTab(entry, (prev, content) => addTabIfAbsent(prev, entry, content), setTabs) if (navSeqRef.current === seq) setActiveTabPath(entry.path) }, []) diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index 7d7df236..c72a9275 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -156,6 +156,7 @@ export const mockHandlers: Record any> = { list_vault: () => MOCK_ENTRIES, reload_vault: () => MOCK_ENTRIES, reload_vault_entry: (args: { path: string }) => MOCK_ENTRIES.find(e => e.path === args.path) ?? { path: args.path, title: 'Unknown', filename: 'unknown.md', aliases: [], belongsTo: [], relatedTo: [], archived: false, trashed: false, snippet: '', wordCount: 0, fileSize: 0, relationships: {}, outgoingLinks: [], properties: {} }, + sync_note_title: () => false, get_note_content: (args: { path: string }) => MOCK_CONTENT[args.path] ?? '', get_all_content: () => MOCK_CONTENT, get_file_history: (args: { path: string }) => mockFileHistory(args.path),