fix: queue diff opening until tab activation

This commit is contained in:
lucaronin
2026-04-25 10:22:58 +02:00
parent d0d9d90096
commit dd018cf98b
3 changed files with 98 additions and 40 deletions

View File

@@ -534,8 +534,8 @@ function App() {
},
onToast: (msg) => setToastMessage(msg),
})
const pulseCommitDiffRequestIdRef = useRef(0)
const [pulseCommitDiffRequest, setPulseCommitDiffRequest] = useState<CommitDiffRequest | null>(null)
const pendingDiffRequestIdRef = useRef(0)
const [pendingDiffRequest, setPendingDiffRequest] = useState<CommitDiffRequest | null>(null)
// Note window: auto-open the note from URL params without scanning the whole vault.
const noteWindowOpenedRef = useRef(false)
@@ -600,17 +600,17 @@ function App() {
onSelectNote: notes.handleSelectNote,
})
const queuePulseCommitDiff = useCallback((path: string, commitHash: string) => {
pulseCommitDiffRequestIdRef.current += 1
setPulseCommitDiffRequest({
requestId: pulseCommitDiffRequestIdRef.current,
const queuePendingDiff = useCallback((path: string, commitHash?: string) => {
pendingDiffRequestIdRef.current += 1
setPendingDiffRequest({
requestId: pendingDiffRequestIdRef.current,
path,
commitHash,
})
}, [])
const handlePulseCommitDiffHandled = useCallback((requestId: number) => {
setPulseCommitDiffRequest((current) =>
const handlePendingDiffHandled = useCallback((requestId: number) => {
setPendingDiffRequest((current) =>
current?.requestId === requestId ? null : current,
)
}, [])
@@ -621,7 +621,7 @@ function App() {
if (commitHash) {
const targetPath = entry?.path ?? fullPath
queuePulseCommitDiff(targetPath, commitHash)
queuePendingDiff(targetPath, commitHash)
if (entry) {
void handleSelectNote(entry)
} else {
@@ -633,7 +633,7 @@ function App() {
if (entry) {
void handleSelectNote(entry)
}
}, [entriesByPath, resolvedPath, queuePulseCommitDiff, handleSelectNote, openTabWithContent])
}, [entriesByPath, resolvedPath, queuePendingDiff, handleSelectNote, openTabWithContent])
const handleOpenFavorite = useCallback(async (entry: VaultEntry) => {
await handleReplaceActiveTab(entry)
@@ -812,11 +812,18 @@ function App() {
}
notes.openTabWithContent(entry, previewContent)
if (hasDiff) {
setTimeout(() => diffToggleRef.current(), 50)
queuePendingDiff(entry.path)
} else {
setToastMessage('Content not available (untracked)')
}
}, [vault, notes, setToastMessage])
}, [vault, notes, queuePendingDiff, setToastMessage])
const handleReplaceActiveTabWithQueuedDiff = useCallback((entry: VaultEntry) => {
notes.handleReplaceActiveTab(entry)
if (effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'changes') {
queuePendingDiff(entry.path)
}
}, [effectiveSelection, notes, queuePendingDiff])
const commitFlow = useCommitFlow({
savePending: appSave.savePending,
@@ -1444,7 +1451,7 @@ function App() {
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
<PulseView vaultPath={resolvedPath} onOpenNote={handlePulseOpenNote} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => handleSetViewMode('all')} />
) : (
<NoteList entries={vault.entries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onAutoTriggerDiff={() => diffToggleRef.current()} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} multiSelectionCommandRef={multiSelectionCommandRef} />
<NoteList entries={vault.entries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} multiSelectionCommandRef={multiSelectionCommandRef} />
)}
</div>
<ResizeHandle onResize={layout.handleNoteListResize} />
@@ -1458,8 +1465,8 @@ function App() {
onNavigateWikilink={notes.handleNavigateWikilink}
onLoadDiff={vault.loadDiff}
onLoadDiffAtCommit={vault.loadDiffAtCommit}
pendingCommitDiffRequest={pulseCommitDiffRequest}
onPendingCommitDiffHandled={handlePulseCommitDiffHandled}
pendingCommitDiffRequest={pendingDiffRequest}
onPendingCommitDiffHandled={handlePendingDiffHandled}
getNoteStatus={vault.getNoteStatus}
onCreateNote={notes.handleCreateNoteImmediate}
inspectorCollapsed={layout.inspectorCollapsed}

View File

@@ -35,6 +35,43 @@ describe('useDiffMode', () => {
warn.mockRestore()
}
async function expectPendingDiffRequestLoaded(options: {
diffContent: string
requestId: number
commitHash?: string
}) {
const { diffContent, requestId, commitHash = '' } = options
const onPendingCommitDiffHandled = vi.fn()
if (commitHash) {
onLoadDiffAtCommit.mockResolvedValue(diffContent)
} else {
onLoadDiff.mockResolvedValue(diffContent)
}
const { result } = renderHook(() => useDiffMode({
activeTabPath: '/note.md',
onLoadDiff,
onLoadDiffAtCommit,
pendingCommitDiffRequest: { requestId, path: '/note.md', commitHash },
onPendingCommitDiffHandled,
}))
await waitFor(() => {
if (commitHash) {
expect(onLoadDiffAtCommit).toHaveBeenCalledWith('/note.md', commitHash)
} else {
expect(onLoadDiff).toHaveBeenCalledWith('/note.md')
expect(onLoadDiffAtCommit).not.toHaveBeenCalled()
}
})
await waitFor(() => {
expect(result.current.diffMode).toBe(true)
expect(result.current.diffContent).toBe(diffContent)
expect(onPendingCommitDiffHandled).toHaveBeenCalledWith(requestId)
})
}
it('starts with diff mode off', () => {
const { result } = renderDiffHook()
expect(result.current.diffMode).toBe(false)
@@ -121,23 +158,17 @@ describe('useDiffMode', () => {
})
it('loads a pending commit diff request when the matching tab is active', async () => {
onLoadDiffAtCommit.mockResolvedValue('pulse diff')
const onPendingCommitDiffHandled = vi.fn()
const { result } = renderHook(() => useDiffMode({
activeTabPath: '/note.md',
onLoadDiffAtCommit,
pendingCommitDiffRequest: { requestId: 7, path: '/note.md', commitHash: 'abc123' },
onPendingCommitDiffHandled,
}))
await waitFor(() => {
expect(onLoadDiffAtCommit).toHaveBeenCalledWith('/note.md', 'abc123')
await expectPendingDiffRequestLoaded({
diffContent: 'pulse diff',
requestId: 7,
commitHash: 'abc123',
})
await waitFor(() => {
expect(result.current.diffMode).toBe(true)
expect(result.current.diffContent).toBe('pulse diff')
expect(onPendingCommitDiffHandled).toHaveBeenCalledWith(7)
})
it('loads a pending working-tree diff request when the matching tab is active', async () => {
await expectPendingDiffRequestLoaded({
diffContent: 'working tree diff',
requestId: 9,
})
})

View File

@@ -9,7 +9,7 @@ import {
export interface CommitDiffRequest {
requestId: number
path: string
commitHash: string
commitHash?: string | null
}
interface UseDiffModeParams {
@@ -73,6 +73,10 @@ function shouldHandlePendingCommitDiffRequest(
return !!pendingCommitDiffRequest && pendingCommitDiffRequest.path === activeTabPath
}
function hasCommitHash(pendingCommitDiffRequest: CommitDiffRequest): pendingCommitDiffRequest is CommitDiffRequest & { commitHash: string } {
return typeof pendingCommitDiffRequest.commitHash === 'string' && pendingCommitDiffRequest.commitHash.length > 0
}
function buildGuardedDiffStateSetters(
cancelledRef: { current: boolean },
{ setDiffMode, setDiffContent, setDiffLoading, setDiffPath }: DiffStateSetters,
@@ -87,18 +91,27 @@ function buildGuardedDiffStateSetters(
function runPendingCommitDiffRequest(
pendingCommitDiffRequest: CommitDiffRequest,
onLoadDiff: ((path: string) => Promise<string>) | undefined,
onLoadDiffAtCommit: (path: string, commitHash: string) => Promise<string>,
onPendingCommitDiffHandled: ((requestId: number) => void) | undefined,
diffState: DiffStateSetters,
) {
const cancelledRef = { current: false }
void loadCommitDiffForPath(
pendingCommitDiffRequest.path,
pendingCommitDiffRequest.commitHash,
onLoadDiffAtCommit,
buildGuardedDiffStateSetters(cancelledRef, diffState),
).finally(() => {
const loadDiffPromise = hasCommitHash(pendingCommitDiffRequest)
? loadCommitDiffForPath(
pendingCommitDiffRequest.path,
pendingCommitDiffRequest.commitHash,
onLoadDiffAtCommit,
buildGuardedDiffStateSetters(cancelledRef, diffState),
)
: loadDiffForPath(
pendingCommitDiffRequest.path,
onLoadDiff,
buildGuardedDiffStateSetters(cancelledRef, diffState),
)
void loadDiffPromise.finally(() => {
if (cancelledRef.current) return
onPendingCommitDiffHandled?.(pendingCommitDiffRequest.requestId)
})
@@ -110,6 +123,7 @@ function runPendingCommitDiffRequest(
function usePendingCommitDiffRequest({
activeTabPath,
onLoadDiff,
onLoadDiffAtCommit,
pendingCommitDiffRequest,
onPendingCommitDiffHandled,
@@ -120,18 +134,23 @@ function usePendingCommitDiffRequest({
}: UseDiffModeParams & DiffStateSetters) {
useEffect(() => {
if (!shouldHandlePendingCommitDiffRequest(activeTabPath, pendingCommitDiffRequest)) return
if (!onLoadDiffAtCommit) {
if (hasCommitHash(pendingCommitDiffRequest) && !onLoadDiffAtCommit) {
onPendingCommitDiffHandled?.(pendingCommitDiffRequest.requestId)
return
}
if (!hasCommitHash(pendingCommitDiffRequest) && !onLoadDiff) {
onPendingCommitDiffHandled?.(pendingCommitDiffRequest.requestId)
return
}
return runPendingCommitDiffRequest(
pendingCommitDiffRequest,
onLoadDiff,
onLoadDiffAtCommit,
onPendingCommitDiffHandled,
{ setDiffMode, setDiffContent, setDiffLoading, setDiffPath },
)
}, [activeTabPath, onLoadDiffAtCommit, onPendingCommitDiffHandled, pendingCommitDiffRequest, setDiffContent, setDiffLoading, setDiffMode, setDiffPath])
}, [activeTabPath, onLoadDiff, onLoadDiffAtCommit, onPendingCommitDiffHandled, pendingCommitDiffRequest, setDiffContent, setDiffLoading, setDiffMode, setDiffPath])
}
export function useDiffMode({
@@ -148,6 +167,7 @@ export function useDiffMode({
usePendingCommitDiffRequest({
activeTabPath,
onLoadDiff,
onLoadDiffAtCommit,
pendingCommitDiffRequest,
onPendingCommitDiffHandled,