Merge branch 'main' into main

This commit is contained in:
github-actions[bot]
2026-04-29 18:55:44 +00:00
committed by GitHub
8 changed files with 82 additions and 59 deletions

View File

@@ -374,7 +374,10 @@ function App() {
}, [resolvedPath, setToastMessage])
const vault = useVaultLoader(noteWindowParams ? '' : resolvedPath)
const recentVaultWrites = useRecentVaultWrites({ vaultPath: noteWindowParams ? '' : resolvedPath })
const {
markInternalWrite: markRecentVaultWrite,
filterExternalPaths: filterExternalVaultPaths,
} = useRecentVaultWrites({ vaultPath: noteWindowParams ? '' : resolvedPath })
const {
status: vaultAiGuidanceStatus,
refresh: refreshVaultAiGuidance,
@@ -562,6 +565,14 @@ function App() {
flushPendingRawContentRef.current?.(path)
await appSave.flushBeforeAction(path)
}
const handleCreatedVaultEntryPersisting = useCallback((path: string) => {
markRecentVaultWrite(path)
vault.addPendingSave(path)
}, [markRecentVaultWrite, vault])
const handleCreatedVaultEntryPersisted = useCallback((path: string) => {
markRecentVaultWrite(path)
vault.loadModifiedFiles()
}, [markRecentVaultWrite, vault])
const notes = useNoteActions({
addEntry: vault.addEntry,
@@ -573,13 +584,13 @@ function App() {
setToastMessage,
updateEntry: vault.updateEntry,
vaultPath: resolvedPath,
addPendingSave: vault.addPendingSave,
addPendingSave: handleCreatedVaultEntryPersisting,
removePendingSave: vault.removePendingSave,
trackUnsaved: vault.trackUnsaved,
clearUnsaved: vault.clearUnsaved,
unsavedPaths: vault.unsavedPaths,
markContentPending: (path, content) => appSave.contentChangeRef.current(path, content),
onNewNotePersisted: vault.loadModifiedFiles,
onNewNotePersisted: handleCreatedVaultEntryPersisted,
onTypeStateChanged: async () => { await vault.reloadVault() },
replaceEntry: vault.replaceEntry,
onFrontmatterPersisted: vault.loadModifiedFiles,
@@ -620,7 +631,7 @@ function App() {
useVaultWatcher({
vaultPath: noteWindowParams ? '' : resolvedPath,
onVaultChanged: handlePulledVaultUpdate,
filterChangedPaths: recentVaultWrites.filterExternalPaths,
filterChangedPaths: filterExternalVaultPaths,
})
const autoSync = useAutoSync({
enabled: gitRepoState === 'ready',
@@ -773,7 +784,7 @@ function App() {
handleRenameNote: notes.handleRenameNote, handleRenameFilename: notes.handleRenameFilename,
replaceEntry: vault.replaceEntry, resolvedPath,
initialH1AutoRenameEnabled: settings.initial_h1_auto_rename_enabled !== false,
onInternalVaultWrite: recentVaultWrites.markInternalWrite,
onInternalVaultWrite: markRecentVaultWrite,
locale: appLocale,
})

View File

@@ -38,6 +38,7 @@ describe('useAppSave', () => {
handleSwitchTab: vi.fn(),
setToastMessage: vi.fn(),
loadModifiedFiles: vi.fn(),
onInternalVaultWrite: vi.fn(),
trackUnsaved: vi.fn(),
clearUnsaved: vi.fn(),
unsavedPaths: new Set<string>(),
@@ -58,6 +59,7 @@ describe('useAppSave', () => {
deps.tabs = []
deps.activeTabPath = null
deps.trackUnsaved.mockReset()
deps.onInternalVaultWrite.mockReset()
deps.handleRenameNote.mockResolvedValue(undefined)
deps.handleRenameFilename.mockResolvedValue(undefined)
deps.initialH1AutoRenameEnabled = true
@@ -581,6 +583,18 @@ describe('useAppSave', () => {
expect(getTabs()[0].content).toBe('# Fresh Title\n\nBody that keeps changing while rename is pending')
})
it('marks untitled auto-rename paths as internal vault writes', async () => {
const { result, oldPath, newPath } = setupUntitledRenameHarness()
await act(async () => {
result.current.handleContentChange(oldPath, '# Fresh Title\n\nBody')
await vi.advanceTimersByTimeAsync(3_000)
})
expect(deps.onInternalVaultWrite).toHaveBeenCalledWith(oldPath)
expect(deps.onInternalVaultWrite).toHaveBeenCalledWith(newPath)
})
it('does not run markdown title-sync renames for non-markdown text files', async () => {
vi.mocked(isTauri).mockReturnValue(true)

View File

@@ -253,6 +253,7 @@ function useUntitledRenameExecutor({
handleSwitchTab,
replaceEntry,
loadModifiedFiles,
onInternalVaultWrite,
renamedPathsRef,
inFlightUntitledRenameRef,
}: {
@@ -263,6 +264,7 @@ function useUntitledRenameExecutor({
handleSwitchTab: AppSaveDeps['handleSwitchTab']
replaceEntry: AppSaveDeps['replaceEntry']
loadModifiedFiles: AppSaveDeps['loadModifiedFiles']
onInternalVaultWrite?: AppSaveDeps['onInternalVaultWrite']
renamedPathsRef: MutableRefObject<RenamedPathMap>
inFlightUntitledRenameRef: MutableRefObject<InFlightRenameMap>
}) {
@@ -277,6 +279,8 @@ function useUntitledRenameExecutor({
notePath: path,
})
if (!result) return path
onInternalVaultWrite?.(path)
onInternalVaultWrite?.(result.new_path)
trackRenamedPath(renamedPathsRef.current, path, result.new_path)
await reloadAutoRenamedNote({
oldPath: path,
@@ -306,6 +310,7 @@ function useUntitledRenameExecutor({
handleSwitchTab,
replaceEntry,
loadModifiedFiles,
onInternalVaultWrite,
renamedPathsRef,
inFlightUntitledRenameRef,
])
@@ -361,6 +366,7 @@ function useUntitledRenameCoordinator({
handleSwitchTab,
replaceEntry,
loadModifiedFiles,
onInternalVaultWrite,
initialH1AutoRenameEnabled,
}: {
resolvedPath: string
@@ -370,6 +376,7 @@ function useUntitledRenameCoordinator({
handleSwitchTab: AppSaveDeps['handleSwitchTab']
replaceEntry: AppSaveDeps['replaceEntry']
loadModifiedFiles: AppSaveDeps['loadModifiedFiles']
onInternalVaultWrite?: AppSaveDeps['onInternalVaultWrite']
initialH1AutoRenameEnabled: boolean
}) {
const {
@@ -387,6 +394,7 @@ function useUntitledRenameCoordinator({
handleSwitchTab,
replaceEntry,
loadModifiedFiles,
onInternalVaultWrite,
renamedPathsRef,
inFlightUntitledRenameRef,
})
@@ -770,11 +778,10 @@ function useAppSaveHandlers({
}
export function useAppSave({
updateEntry, setTabs, handleSwitchTab, setToastMessage, loadModifiedFiles, reloadViews,
trackUnsaved, clearUnsaved, unsavedPaths, tabs, activeTabPath, handleRenameNote,
handleRenameFilename: handleRenameFilenameRaw, replaceEntry, resolvedPath,
initialH1AutoRenameEnabled,
onInternalVaultWrite,
updateEntry, setTabs, handleSwitchTab, setToastMessage, loadModifiedFiles,
reloadViews, trackUnsaved, clearUnsaved, unsavedPaths, tabs, activeTabPath,
handleRenameNote, handleRenameFilename: handleRenameFilenameRaw, replaceEntry,
resolvedPath, initialH1AutoRenameEnabled, onInternalVaultWrite,
locale = 'en',
}: AppSaveDeps) {
const contentChangeRef = useRef<(path: string, content: string) => void>(() => {})
@@ -784,58 +791,26 @@ export function useAppSave({
pendingUntitledRenameRef, cancelPendingUntitledRename, registerRenamedPath,
resolveCurrentPath, resolvePathBeforeSave, flushPendingUntitledRename, scheduleUntitledRename,
} = useUntitledRenameCoordinator({
resolvedPath,
tabsRef,
activeTabPathRef,
setTabs,
handleSwitchTab,
replaceEntry,
loadModifiedFiles,
initialH1AutoRenameEnabled,
resolvedPath, tabsRef, activeTabPathRef, setTabs, handleSwitchTab,
replaceEntry, loadModifiedFiles, onInternalVaultWrite, initialH1AutoRenameEnabled,
})
const { handleSaveRaw, handleContentChange, savePendingForPath, savePending } = useEditorPersistence({
updateEntry,
setTabs,
setToastMessage,
loadModifiedFiles,
trackUnsaved,
clearUnsaved,
onInternalVaultWrite,
reloadViews,
scheduleUntitledRename,
resolveCurrentPath,
resolvePathBeforeSave,
canPersist,
updateEntry, setTabs, setToastMessage, loadModifiedFiles, trackUnsaved,
clearUnsaved, onInternalVaultWrite, reloadViews, scheduleUntitledRename,
resolveCurrentPath, resolvePathBeforeSave, canPersist,
locale,
})
const replaceRenamedEntry = useReplaceRenamedEntry({ registerRenamedPath, replaceEntry })
const { handleFilenameRename, handleSave, handleTitleSync, flushBeforeAction } = useAppSaveHandlers({
contentChangeRef,
handleContentChange,
canPersist,
cancelPendingUntitledRename,
pendingUntitledRenameRef,
activeTabPath,
resolveCurrentPath,
savePendingForPath,
tabsRef,
unsavedPathsRef,
clearUnsaved,
setToastMessage,
flushPendingUntitledRename,
locale,
handleRenameNote,
contentChangeRef, handleContentChange, canPersist, cancelPendingUntitledRename,
pendingUntitledRenameRef, activeTabPath, resolveCurrentPath, savePendingForPath,
tabsRef, unsavedPathsRef, clearUnsaved, setToastMessage, flushPendingUntitledRename, locale, handleRenameNote,
handleRenameFilename: handleRenameFilenameRaw,
resolvedPath,
replaceRenamedEntry,
loadModifiedFiles,
handleSaveRaw,
tabs,
unsavedPaths,
resolvedPath, replaceRenamedEntry, loadModifiedFiles, handleSaveRaw, tabs, unsavedPaths,
})
return {
contentChangeRef, handleContentChange, handleFilenameRename, handleSave, handleTitleSync,
savePending, savePendingForPath, trackRenamedPath: registerRenamedPath, flushBeforeAction,
contentChangeRef, handleContentChange, handleFilenameRename, handleSave,
handleTitleSync, savePending, savePendingForPath, trackRenamedPath: registerRenamedPath, flushBeforeAction,
}
}

View File

@@ -380,6 +380,7 @@ describe('useNoteActions hook', () => {
expect(addPendingSave).toHaveBeenCalledWith(createdPath)
expect(removePendingSave).toHaveBeenCalledWith(createdPath)
expect(onNewNotePersisted).toHaveBeenCalledOnce()
expect(onNewNotePersisted).toHaveBeenCalledWith(createdPath)
expect(addEntry).toHaveBeenCalledTimes(1)
expect(result.current.tabs[0].entry.path).toMatch(/untitled-note-\d+\.md$/)
})
@@ -397,6 +398,7 @@ describe('useNoteActions hook', () => {
})
expect(onNewNotePersisted).toHaveBeenCalledTimes(1)
expect(onNewNotePersisted).toHaveBeenCalledWith(expect.stringContaining('persist-callback.md'))
})
it('does not call onNewNotePersisted when disk write fails (Tauri)', async () => {

View File

@@ -30,7 +30,7 @@ export interface NoteActionsConfig {
clearUnsaved?: (path: string) => void
unsavedPaths?: Set<string>
markContentPending?: (path: string, content: string) => void
onNewNotePersisted?: () => void
onNewNotePersisted?: (path: string) => void
replaceEntry?: (oldPath: string, patch: Partial<VaultEntry> & { path: string }) => void
onPathRenamed?: (oldPath: string, newPath: string) => void
/** Called after frontmatter is written to disk — used for live-reloading theme CSS vars. */

View File

@@ -382,6 +382,7 @@ describe('useNoteCreation hook', () => {
expect(addPendingSave).toHaveBeenCalledWith(createdPath)
expect(removePendingSave).toHaveBeenCalledWith(createdPath)
expect(onNewNotePersisted).toHaveBeenCalledOnce()
expect(onNewNotePersisted).toHaveBeenCalledWith(createdPath)
expect(addEntry).toHaveBeenCalledTimes(1)
expect(openTabWithContent).toHaveBeenCalledTimes(1)
expect(vi.mocked(invoke).mock.invocationCallOrder[0]).toBeLessThan(

View File

@@ -284,7 +284,7 @@ function signalFocusEditor(opts?: { selectTitle?: boolean; path?: string }): voi
interface PersistCallbacks {
onStart?: (p: string) => void
onEnd?: (p: string) => void
onPersisted?: () => void
onPersisted?: (path: string) => void
}
/** Persist to disk; track pending state via onStart/onEnd. */
@@ -292,7 +292,7 @@ async function persistOptimistic(path: string, content: string, cbs: PersistCall
cbs.onStart?.(path)
try {
await persistNewNote(path, content)
cbs.onPersisted?.()
cbs.onPersisted?.(path)
} finally {
cbs.onEnd?.(path)
}
@@ -410,7 +410,7 @@ interface ImmediateCreateDeps {
pendingSlugs: Set<string>
openTabWithContent: (entry: VaultEntry, content: string) => void
addEntry: (entry: VaultEntry) => void
onNewNotePersisted?: () => void
onNewNotePersisted?: (path: string) => void
removePendingSave?: (path: string) => void
setToastMessage: (msg: string | null) => void
}
@@ -425,7 +425,7 @@ interface ImmediateCreateQueueConfig {
vaultPath: string
addEntry: (entry: VaultEntry) => void
openTabWithContent: (entry: VaultEntry, content: string) => void
onNewNotePersisted?: () => void
onNewNotePersisted?: (path: string) => void
removePendingSave?: (path: string) => void
setToastMessage: (msg: string | null) => void
}
@@ -600,7 +600,7 @@ export interface NoteCreationConfig {
clearUnsaved?: (path: string) => void
unsavedPaths?: Set<string>
markContentPending?: (path: string, content: string) => void
onNewNotePersisted?: () => void
onNewNotePersisted?: (path: string) => void
onTypeStateChanged?: () => void | Promise<void>
}

View File

@@ -189,6 +189,26 @@ test('@smoke new-note H1 auto-rename keeps the editor usable and leaves no untit
expect(errors).toEqual([])
})
test('@smoke new-note typing stays focused through initial save settlement', async ({ page }) => {
const title = 'Creation Focus Guard'
const bodyText = 'Body keeps accepting text while creation writes and saves settle.'
await createUntitledNote(page)
await page.keyboard.type(title, { delay: 35 })
await page.keyboard.press('Enter')
await page.keyboard.type(bodyText, { delay: 35 })
await page.waitForTimeout(1_000)
await expectEditorFocused(page)
await page.keyboard.type(' Still focused.')
await expectActiveFilename(page, slugifyTitle(title))
await expectFileContentContains({
vaultPath: tempVaultDir,
filename: `${slugifyTitle(title)}.md`,
text: 'Still focused.',
})
})
test('@smoke new-note H1 auto-rename preserves body typing and cursor while rename lands', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (err) => {